aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog4
-rw-r--r--lib/specs.exp294
-rw-r--r--testsuite/runtest.libs/specs.test162
3 files changed, 460 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
index 8313db3..7f4f1f4 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,7 @@
+2021-04-14 Jacob Bachmeyer <jcb@gnu.org>
+
+ * lib/specs.exp, testsuite/runtest.libs/specs.test: New files.
+
2021-03-22 Jacob Bachmeyer <jcb@gnu.org>
* configure: Regenerate.
diff --git a/lib/specs.exp b/lib/specs.exp
new file mode 100644
index 0000000..457b4ed
--- /dev/null
+++ b/lib/specs.exp
@@ -0,0 +1,294 @@
+# Copyright (C) 2021 Free Software Foundation, Inc.
+#
+# This file is part of DejaGnu.
+#
+# DejaGnu is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# DejaGnu is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with DejaGnu; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+
+# This file was written by Jacob Bachmeyer.
+
+# Procedures for handling specs strings similar to those used in GCC.
+
+# These spec strings support substitutions introduced using "%":
+#
+# %% -- literal "%" character
+# %{...} -- substitute data value with recursive evaluation
+# %[...] -- evaluate Tcl code and substitute result literally
+#
+# All other uses of "%" in specs strings are reserved. Data item names
+# containing colon (":") are generally reserved for future expansion; a few
+# are currently used as shorthand for certain DejaGnu API calls.
+# Convention for hierarchical name parts is separation using ".", while "/"
+# is used for variations intended to be selected using another value.
+
+# Specs are stored in a Tcl array, referred to as the "database" array.
+# Spec strings are organized into layers, providing a hierarchical
+# structure of fallback and default values by searching layers in the order
+# given by the "_layers" option.
+
+# The external data structures used by this module are mostly association
+# lists, but they are internally referenced using Tcl arrays.
+
+# All procedures in this module are currently internal to DejaGnu and
+# subject to change without notice.
+namespace eval ::dejagnu::specs {
+ namespace export eval_specs validate_specs
+}
+
+# Expand one data substitution token.
+# internal procedure; uses SPECS and OPTION arrays in caller's context
+proc ::dejagnu::specs::subst_token { key } {
+ upvar 1 specs specs option option
+
+ # check for an option first
+ if { [info exists option($key)] } {
+ return $option($key)
+ }
+
+ # check for a board configuration value
+ if { [regexp {^board_info\(([^)]+)\):(.*)$} $key -> machine info_key] } {
+ return [board_info $machine $info_key]
+ }
+
+ # search the specs database if a layer path was given
+ if { [info exists option(_layers)] } {
+ foreach layer $option(_layers) {
+ if { [info exists specs(layer,$layer,$key)] } {
+ return $specs(layer,$layer,$key)
+ }
+ }
+ }
+
+ # check for suitable default entry in the specs database
+ if { [info exists specs(base,$key)] } {
+ return $specs(base,$key)
+ }
+
+ error "unresolved specs token: $key"
+}
+
+# Evaluate excess open or close delimiters.
+proc ::dejagnu::specs::delimiter_balance { text } {
+ # first, remove all backslashes that cannot quote delimiters
+ regsub -all {\\+[^][\\{}]} $text "" text
+ # strip backslash-quoted backslashes
+ regsub -all {(?:\\{2})+} $text "" text
+ # strip backslash-quoted delimiters
+ regsub -all {(^|[^\\])\\[][{}]} $text "\\1" text
+ # remove all unrelated characters
+ regsub -all {[^][{}]+} $text "" text
+
+ # separate the text into only-left and only-right subsets
+ regsub -all "\\\\*\[\]\}\]" $text "" left
+ regsub -all "\\\\*\[\[\{\]" $text "" right
+
+ return [expr { [string length $left] - [string length $right] }]
+}
+
+# Find the end of a token.
+proc ::dejagnu::specs::token_end { text start end_pat } {
+ set balance 1
+ set point $start
+ while { $balance > 0 } {
+ regexp -indices -start [expr { 1 + $point }] -- $end_pat $text item
+ set point [lindex $item 0]
+ # optimization: if delimiter_balance returns N, we need at least N
+ # more closing delimiters, but that could be any combination of
+ # braces and brackets, not only the main endpoint delimiter
+ for {
+ set balance [delimiter_balance [string range $text $start $point]]
+ } { $balance > 1 } { incr balance -1 } {
+ regexp -indices -start [expr { 1 + $point }] -- \
+ "\[\\\}\\\]\]" $text item
+ set point [lindex $item 0]
+ }
+ }
+ return [lindex $item 1]
+}
+
+# Abstract parsing loop.
+# internal procedure; sets TOKEN variable in caller's context
+proc ::dejagnu::specs::scan_specs_string { text literal char data code } {
+ upvar 1 token token
+
+ for {
+ set mark -1
+ set point 0
+ } { [regexp -indices -start $point -- {%.} $text item] } {
+ set point [expr { 1 + $mark }]
+ } {
+ # extract literal from preceding range
+ set token [string range $text \
+ [expr { $mark + 1 }] \
+ [expr { [lindex $item 0] - 1 }]]
+ uplevel 1 $literal
+ # advance point
+ set point [lindex $item 1]
+ # extract first character of substitution
+ set enter [string index $text $point]
+ if { $enter eq "%" } {
+ # %% -- literal "%"
+ set mark $point
+ uplevel 1 $char
+ } elseif { $enter eq "\{" } {
+ # %{...} -- substitute data item
+ set mark [token_end $text $point "\\\}"]
+ set token [string range $text \
+ [expr { $point + 1 }] [expr { $mark - 1 }]]
+ uplevel 1 $data
+ } elseif { $enter eq "\[" } {
+ # %[...] -- substitute value from Tcl code fragment
+ set mark [token_end $text $point "\\\]"]
+ set token [string range $text \
+ [expr { $point + 1 }] [expr { $mark - 1 }]]
+ uplevel 1 $code
+ } else {
+ error "unrecognized sequence %$enter in spec string"
+ }
+ }
+ # leave the trailing literal in TOKEN
+ set token [string range $text [expr { $mark + 1 }] end]
+}
+
+# Generate parse report for specs string; for debugging
+proc ::dejagnu::specs::parse_specs_string { text } {
+ set tokens [list]
+ scan_specs_string $text {
+ # intervening literal text
+ lappend tokens [list text $token]
+ } { # %% escape
+ lappend tokens [list text %]
+ } { # data item
+ lappend tokens [list data $token]
+ } { # code item
+ lappend tokens [list code $token]
+ }
+ lappend tokens [list text $token]
+ return $tokens
+}
+
+# Expand substitutions in specs string.
+# internal procedure; uses SPECS and OPTION arrays and BASE_LEVEL variable
+# in caller's context
+proc ::dejagnu::specs::eval_specs_string { text } {
+ upvar 1 specs specs option option base_level base_level
+
+ set output ""
+ scan_specs_string $text {
+ # copy intervening literal text to output
+ append output $token
+ } {
+ # emit "%" where string contains "%%"
+ append output "%"
+ } {
+ # substitute data item
+ append output [eval_specs_string \
+ [subst_token [eval_specs_string $token]]]
+ } {
+ # evaluate Tcl code fragment
+ append output [uplevel "#$base_level" [eval_specs_string $token]]
+ }
+ # copy trailing literal
+ append output $token
+
+ return $output
+}
+
+# Check that the provided specs string can be evaluated; that is, that all
+# substitutions have definitions.
+# internal procedure; uses SPECS and OPTION arrays in caller's context
+proc ::dejagnu::specs::validate_specs_string { text } {
+ upvar 1 specs specs option option
+
+ scan_specs_string $text {
+ # ignore literal text
+ } {
+ # ignore literal "%"
+ } {
+ # check substitution
+ } {
+ # check Tcl code fragment
+ }
+ # ignore trailing literal
+
+ # an error is thrown if validation fails
+ return 1
+}
+
+# Perform spec substitutions to evaluate %{GOAL}.
+#
+# The DATABASE_NAME is the name (in the caller's context) of the database
+# array to use, while OPTIONS is a list of additional KEY VALUE pairs that
+# should be available for substitution.
+proc ::dejagnu::specs::eval_specs { database_name goal options } {
+ upvar 1 $database_name specs
+ array set option $options
+ set base_level [expr { [info level] - 1 }]
+
+ return [eval_specs_string "%{$goal}"]
+}
+
+# Load specs strings into DATABASE_NAME; as:
+# load_specs DATABASE_NAME BASE_STRINGS (LAYER_NAME LAYER_STRINGS)...
+# to load only into a layer:
+# load_specs DATABASE_NAME {} LAYER_NAME LAYER_STRINGS
+proc ::dejagnu::specs::load_specs { database_name base_strings args } {
+ upvar 1 $database_name specs
+
+ if { ([llength $args] & 1) != 0 } {
+ error "specs layer names and contents must be in pairs"
+ }
+ foreach {k v} $base_strings {
+ set specs(base,$k) $v
+ }
+ foreach {layer layer_strings} $args {
+ foreach {k v} $layer_strings {
+ set specs(layer,$layer,$k) $v
+ }
+ }
+}
+
+# Display contents of specs database array; for debugging
+proc ::dejagnu::specs::dump_specs { database_name } {
+ upvar 1 $database_name specs
+
+ set keys [lsort -dictionary [array names specs]]
+ # all defaults (base,*) sort ahead of all layers (layer,*,*)
+
+ puts "Specs $database_name:\n"
+ for { set i 0 } { ($i < [llength $keys])
+ && [regexp {^base,(.*)$} [lindex $keys $i] \
+ -> name] } \
+ { incr i } {
+ puts "*$name:\n$specs([lindex $keys $i])\n"
+ }
+
+ for { set prev "" } { ($i < [llength $keys])
+ && [regexp {^layer,([^,]+),(.*)$} [lindex $keys $i] \
+ -> layer name] } \
+ { incr i } {
+ if { $prev ne $layer } {
+ puts "\[$layer\]"
+ set prev $layer
+ }
+ puts "*$name:\n$specs([lindex $keys $i])\n"
+ }
+}
+
+# Validate a specs database
+proc ::dejagnu::specs::validate_specs { database_name } {
+ upvar 1 $database_name specs
+
+ # TODO
+}
diff --git a/testsuite/runtest.libs/specs.test b/testsuite/runtest.libs/specs.test
new file mode 100644
index 0000000..d6eac07
--- /dev/null
+++ b/testsuite/runtest.libs/specs.test
@@ -0,0 +1,162 @@
+# Test procedures in lib/specs.exp -*- Tcl -*-
+
+# Copyright (C) 2021 Free Software Foundation, Inc.
+#
+# This file is part of DejaGnu.
+#
+# DejaGnu is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# DejaGnu is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with DejaGnu; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+
+source $srcdir/$subdir/default_procs.tcl
+
+proc load_lib { lib } {
+ global srcdir
+ source $srcdir/../lib/$lib
+}
+
+foreach lib { targetdb specs } {
+ source $srcdir/../lib/${lib}.exp
+}
+
+#
+# Create a false board config array
+#
+set board_info(baz,name) "baz"
+set board_info(baz,ldscript) "-Tbaz.ld"
+set board_info(quux,name) "quux"
+set board_info(quux,ldscript) "-specs quux.specs"
+set board_info(quux,other) "-mquux"
+
+::dejagnu::specs::load_specs test_specs {
+ one 1
+ two 2
+ three 3
+
+ percent {%%}
+
+ base_test {%{two} %{one} %{three}}
+ esc_test_1 {%{two} %% %{three}}
+ esc_test_2 {%{one} %{percent} %{three}}
+
+
+ mapped/asc {%{one} %{two} %{three}}
+ mapped/desc {%{three} %{two} %{one}}
+
+ mapped/ {%{mapped/asc}}
+ mapped_order asc
+
+ map_test {%{mapped/%{mapped_order}}}
+
+
+ args {}
+
+ call_test_1 {%[test_proc_1]}
+ call_test_2 {%[test_proc_2 %{args}]}
+
+
+ board {}
+ key {ldscript}
+
+ board_test {%{board_info(%{board}):%{key}}}
+} foo {
+ one 4
+ three 6
+} bar {
+ two 8
+ three 9
+}
+
+# test procedures for %[...] tests
+proc test_proc_1 {} { return "test-1" }
+proc test_proc_2 { args } { return "[llength $args]: $args" }
+
+# simple wrapper to bring global spec database into current scope
+proc eval_specs { database_name goal options } {
+ global $database_name
+ ::dejagnu::specs::eval_specs $database_name $goal $options
+}
+
+run_tests {
+ { "#" simple substitutions }
+ { lib_ret_test eval_specs {test_specs base_test {}}
+ {2 1 3}
+ "evaluate simple spec substitutions" }
+ { lib_ret_test eval_specs {test_specs base_test {one 5}}
+ {2 5 3}
+ "evaluate simple spec substitutions with option as override" }
+ { lib_ret_test eval_specs {test_specs esc_test_1 {}}
+ {2 % 3}
+ "evaluate simple spec substitutions with literal %" }
+ { lib_ret_test eval_specs {test_specs esc_test_2 {}}
+ {1 % 3}
+ "evaluate simple spec substitutions with literal % substituted" }
+
+ { "#" layer search path }
+ { lib_ret_test eval_specs {test_specs base_test {_layers {foo}}}
+ {2 4 6}
+ "use layer 'foo'" }
+ { lib_ret_test eval_specs {test_specs base_test {_layers {bar}}}
+ {8 1 9}
+ "use layer 'bar'" }
+ { lib_ret_test eval_specs {test_specs base_test {_layers {foo bar}}}
+ {8 4 6}
+ "use layers 'foo' and 'bar'" }
+ { lib_ret_test eval_specs {test_specs base_test {_layers {bar foo}}}
+ {8 4 9}
+ "use layers 'bar' and 'foo'" }
+
+ { "#" value-map substitutions }
+ { lib_ret_test eval_specs {test_specs map_test {}}
+ {1 2 3}
+ "mapped-value substitution as default" }
+ { lib_ret_test eval_specs {test_specs map_test {mapped_order desc}}
+ {3 2 1}
+ "mapped-value substitution with option as override" }
+ { lib_ret_test eval_specs {test_specs map_test {mapped_order ""}}
+ {1 2 3}
+ "mapped-value substitution with empty selector" }
+ { lib_errpat_test eval_specs {test_specs map_test {mapped_order bogus}}
+ {*mapped/bogus*}
+ "error if mapped value refers to non-existent spec string" }
+
+ { "#" procedure-call substitutions }
+ { lib_ret_test eval_specs {test_specs call_test_1 {}}
+ {test-1}
+ "substitute arity 0 procedure call result" }
+ { lib_ret_test eval_specs {test_specs call_test_2 {}}
+ {0: }
+ "substitute procedure call result with no arguments" }
+ { lib_ret_test eval_specs {test_specs call_test_2 {args {%{base_test}}}}
+ {3: 2 1 3}
+ "substitute procedure call result with substituted arguments" }
+ { lib_ret_test eval_specs {test_specs call_test_2 {args {%%{one}}}}
+ {1: %{one}}
+ "substitutions not evaluated in procedure call result" }
+
+ { "#" board_info substitutions }
+ { lib_ret_test eval_specs {test_specs board_test {board baz key other}}
+ {}
+ "empty result for non-existent key" }
+ { lib_ret_test eval_specs {test_specs board_test {board baz}}
+ {-Tbaz.ld}
+ "find 'ldscript' key for board 'baz'" }
+ { lib_ret_test eval_specs {test_specs board_test {board quux}}
+ {-specs quux.specs}
+ "find 'ldscript' key for board 'quux'" }
+ { lib_ret_test eval_specs {test_specs board_test {board quux key other}}
+ {-mquux}
+ "find 'other' key for board 'quux'" }
+}
+
+puts "END specs.test"