gloo 6.7.2 → 7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +1 -0
  3. data/docs/application.md +3 -1
  4. data/docs/iterators.md +39 -3
  5. data/docs/language_syntax.md +53 -5
  6. data/docs/objects.md +58 -1
  7. data/docs/operators.md +16 -0
  8. data/docs/plugins.md +9 -1
  9. data/docs/verbs.md +4 -2
  10. data/lib/VERSION +1 -1
  11. data/lib/VERSION_NOTES +20 -0
  12. data/lib/gloo/app/engine.rb +64 -8
  13. data/lib/gloo/app/log.rb +47 -6
  14. data/lib/gloo/convert/converter.rb +63 -2
  15. data/lib/gloo/convert/string_to_date.rb +14 -0
  16. data/lib/gloo/convert/string_to_datetime.rb +16 -0
  17. data/lib/gloo/convert/string_to_decimal.rb +13 -0
  18. data/lib/gloo/convert/string_to_integer.rb +9 -2
  19. data/lib/gloo/convert/string_to_time.rb +28 -0
  20. data/lib/gloo/core/error.rb +11 -2
  21. data/lib/gloo/core/event_manager.rb +7 -4
  22. data/lib/gloo/core/factory.rb +16 -11
  23. data/lib/gloo/core/here.rb +6 -1
  24. data/lib/gloo/core/invoker.rb +2 -3
  25. data/lib/gloo/core/not_found.rb +42 -0
  26. data/lib/gloo/core/obj.rb +28 -2
  27. data/lib/gloo/core/op.rb +2 -0
  28. data/lib/gloo/core/parser.rb +1 -1
  29. data/lib/gloo/core/pn.rb +33 -12
  30. data/lib/gloo/core/tokens.rb +38 -0
  31. data/lib/gloo/core/verb.rb +54 -0
  32. data/lib/gloo/exec/dispatch.rb +5 -6
  33. data/lib/gloo/exec/exec_env.rb +13 -0
  34. data/lib/gloo/exec/runner.rb +10 -2
  35. data/lib/gloo/exec/script.rb +13 -1
  36. data/lib/gloo/expr/expression.rb +39 -1
  37. data/lib/gloo/expr/op_plus.rb +3 -0
  38. data/lib/gloo/objs/basic/boolean.rb +16 -0
  39. data/lib/gloo/objs/basic/container.rb +154 -6
  40. data/lib/gloo/objs/basic/decimal.rb +1 -0
  41. data/lib/gloo/objs/basic/integer.rb +1 -0
  42. data/lib/gloo/objs/basic/string.rb +0 -1
  43. data/lib/gloo/objs/basic/string_msgs.rb +68 -32
  44. data/lib/gloo/objs/ctrl/each.rb +22 -3
  45. data/lib/gloo/objs/ctrl/each_dir.rb +20 -7
  46. data/lib/gloo/objs/ctrl/each_file.rb +78 -9
  47. data/lib/gloo/objs/ctrl/function.rb +6 -5
  48. data/lib/gloo/objs/dt/date.rb +28 -11
  49. data/lib/gloo/objs/dt/datetime.rb +72 -21
  50. data/lib/gloo/objs/dt/dt_tools.rb +46 -2
  51. data/lib/gloo/objs/dt/time.rb +28 -11
  52. data/lib/gloo/objs/str_utils/cipher.rb +17 -4
  53. data/lib/gloo/objs/str_utils/password.rb +6 -2
  54. data/lib/gloo/objs/system/erb.rb +9 -3
  55. data/lib/gloo/objs/system/file_handle.rb +57 -21
  56. data/lib/gloo/objs/system/system.rb +6 -2
  57. data/lib/gloo/objs/web/http_get.rb +12 -2
  58. data/lib/gloo/objs/web/http_post.rb +6 -2
  59. data/lib/gloo/objs/web/json.rb +38 -18
  60. data/lib/gloo/objs/web/uri.rb +36 -16
  61. data/lib/gloo/persist/file_loader.rb +72 -11
  62. data/lib/gloo/persist/file_saver.rb +2 -1
  63. data/lib/gloo/persist/indent_stack.rb +49 -33
  64. data/lib/gloo/persist/line_splitter.rb +40 -3
  65. data/lib/gloo/persist/persist_man.rb +2 -4
  66. data/lib/gloo/plugin/ext_manager.rb +6 -9
  67. data/lib/gloo/plugin/lib_manager.rb +2 -2
  68. data/lib/gloo/verbs/break.rb +1 -0
  69. data/lib/gloo/verbs/check.rb +1 -1
  70. data/lib/gloo/verbs/cls.rb +1 -0
  71. data/lib/gloo/verbs/create.rb +32 -4
  72. data/lib/gloo/verbs/execute.rb +1 -1
  73. data/lib/gloo/verbs/exists.rb +27 -15
  74. data/lib/gloo/verbs/files.rb +2 -1
  75. data/lib/gloo/verbs/help.rb +1 -0
  76. data/lib/gloo/verbs/if.rb +1 -1
  77. data/lib/gloo/verbs/invoke.rb +1 -1
  78. data/lib/gloo/verbs/list.rb +4 -3
  79. data/lib/gloo/verbs/load.rb +3 -3
  80. data/lib/gloo/verbs/move.rb +10 -8
  81. data/lib/gloo/verbs/put.rb +20 -8
  82. data/lib/gloo/verbs/quit.rb +1 -0
  83. data/lib/gloo/verbs/redirect.rb +8 -2
  84. data/lib/gloo/verbs/reload.rb +9 -1
  85. data/lib/gloo/verbs/run.rb +2 -2
  86. data/lib/gloo/verbs/save.rb +4 -2
  87. data/lib/gloo/verbs/tell.rb +1 -1
  88. data/lib/gloo/verbs/unless.rb +1 -1
  89. data/lib/gloo/verbs/unload.rb +8 -0
  90. data/test.gloo/ctrl/each.test.gloo +88 -0
  91. data/test.gloo/lang/declaration.test.gloo +27 -0
  92. data/test.gloo/lang/exceptions.test.gloo +60 -0
  93. data/test.gloo/lang/failures.test.gloo +139 -0
  94. data/test.gloo/lang/it.test.gloo +133 -0
  95. data/test.gloo/lang/naming.test.gloo +11 -0
  96. data/test.gloo/lang/ops.test.gloo +30 -0
  97. data/test.gloo/objs/can.test.gloo +101 -0
  98. data/test.gloo/objs/string.test.gloo +24 -0
  99. data/test.gloo/string/str.test.gloo +28 -0
  100. data/test.gloo/verbs/create.test.gloo +36 -0
  101. data/test.gloo/verbs/exists.test.gloo +33 -1
  102. data/test.gloo/verbs/unload.test.gloo +34 -0
  103. metadata +4 -1
@@ -16,6 +16,7 @@ module Gloo
16
16
  # This will unload all loaded objects and reset the engine state.
17
17
  #
18
18
  def run
19
+ return if extra_words_err?( 'nothing was unloaded; to unload one object, use tell x to unload' )
19
20
  return unless @engine.persist_man.maps
20
21
 
21
22
  @engine.persist_man.unload_all
@@ -58,6 +59,13 @@ module Gloo
58
59
  'save state prior to executing.',
59
60
  :syntax => [ 'unload' ],
60
61
  :result => 'All objects will be unloaded from the heap.',
62
+ :errors => [
63
+ 'unload takes no object, so nothing was unloaded — anything ' \
64
+ 'written after unload is an error, and nothing is unloaded. ' \
65
+ 'To unload one object, use tell x to unload.'
66
+ ],
67
+ :notes => 'unload takes no object; to unload just one object, ' \
68
+ 'send it the unload message (tell x to unload).',
61
69
  :examples => <<~EXAMPLES.strip
62
70
  > unload
63
71
  EXAMPLES
@@ -35,3 +35,91 @@ tests.ctrl.each [can] :
35
35
  tell ^^.rpt to run
36
36
  eval ^^.count = 3
37
37
  assert "expected repeat to run 3 times"
38
+
39
+ #
40
+ # Each file / each dir over a nested folder tree.
41
+ # The tree is built under /tmp and removed at the end.
42
+ #
43
+
44
+ tests.ctrl.each_folder [can] :
45
+
46
+ count [int] : 0
47
+
48
+ make_tree [system] :
49
+ command [string] : rm -rf /tmp/gloo_each_walk && mkdir -p /tmp/gloo_each_walk/a/deep /tmp/gloo_each_walk/.obs && touch /tmp/gloo_each_walk/top.md /tmp/gloo_each_walk/notes.txt /tmp/gloo_each_walk/a/x.md /tmp/gloo_each_walk/a/deep/Y.MD /tmp/gloo_each_walk/.obs/o.md
50
+ get_output [bool] : false
51
+ remove_tree [system] :
52
+ command [string] : rm -rf /tmp/gloo_each_walk
53
+ get_output [bool] : false
54
+
55
+ files [each] :
56
+ file [file] :
57
+ in [file] : /tmp/gloo_each_walk
58
+ ext [string] : md
59
+ recursive [bool] : true
60
+ do [script] :
61
+ eval ^^.count + 1
62
+ put it into ^^.count
63
+
64
+ wild [each] :
65
+ file [file] :
66
+ in [file] : /tmp/gloo_each_walk/**/
67
+ ext [string] : md
68
+ do [script] :
69
+ eval ^^.count + 1
70
+ put it into ^^.count
71
+
72
+ top [each] :
73
+ file [file] :
74
+ in [file] : /tmp/gloo_each_walk
75
+ do [script] :
76
+ eval ^^.count + 1
77
+ put it into ^^.count
78
+
79
+ dirs [each] :
80
+ dir [file] :
81
+ in [file] : /tmp/gloo_each_walk
82
+ recursive [bool] : true
83
+ do [script] :
84
+ eval ^^.count + 1
85
+ put it into ^^.count
86
+
87
+ recursive_each_file [test] :
88
+ description [string] : Recursive each file finds nested files, ignoring ext case and hidden folders.
89
+ on_test [script] :
90
+ tell ^^.make_tree to run
91
+ put 0 into ^^.count
92
+ tell ^^.files to run
93
+ eval ^^.count = 3
94
+ assert "expected top.md, a/x.md and a/deep/Y.MD"
95
+ tell ^^.remove_tree to run
96
+
97
+ wildcard_in_folder [test] :
98
+ description [string] : A ** wildcard in the folder walks the tree too.
99
+ on_test [script] :
100
+ tell ^^.make_tree to run
101
+ put 0 into ^^.count
102
+ tell ^^.wild to run
103
+ eval ^^.count = 3
104
+ assert "expected the same 3 files as recursive"
105
+ tell ^^.remove_tree to run
106
+
107
+ files_only_by_default [test] :
108
+ description [string] : Each file without ext lists files, not folders.
109
+ on_test [script] :
110
+ tell ^^.make_tree to run
111
+ put 0 into ^^.count
112
+ tell ^^.top to run
113
+ eval ^^.count = 2
114
+ assert "expected top.md and notes.txt, but not the folder a"
115
+ tell ^^.remove_tree to run
116
+
117
+ recursive_each_dir [test] :
118
+ description [string] : Recursive each dir lists every folder in the tree, skipping hidden ones.
119
+ on_test [script] :
120
+ tell ^^.make_tree to run
121
+ put 0 into ^^.count
122
+ tell ^^.dirs to run
123
+ eval ^^.count = 2
124
+ assert "expected a and a/deep"
125
+ tell ^^.remove_tree to run
@@ -0,0 +1,27 @@
1
+ #
2
+ # Declaration syntax tests
3
+ #
4
+
5
+ tests.lang.declaration [can] :
6
+
7
+ # Deliberately missing the ']' after its type: loading reports a
8
+ # syntax error and reads the type as 'int' anyway.
9
+ unclosed [int : 7
10
+
11
+ closed_then_colon [int]: 8
12
+
13
+ type_missing_its_closing_bracket [test] :
14
+ description [string] : a type missing its ']' is still read as that type, with its value.
15
+ on_test [script] :
16
+ eval ^^.unclosed = 7
17
+ assert "expected the value 7"
18
+
19
+ put '12' into ^^.unclosed
20
+ eval ^^.unclosed = 12
21
+ assert "expected it to still be an integer"
22
+
23
+ closed_type_followed_by_the_colon [test] :
24
+ description [string] : a ']' followed directly by ':' is a normal declaration.
25
+ on_test [script] :
26
+ eval ^^.closed_then_colon = 8
27
+ assert "expected the value 8"
@@ -20,6 +20,8 @@ tests.lang.exceptions [can] :
20
20
  error_data [can] :
21
21
  message [string] :
22
22
  backtrace [string] :
23
+ kind [string] :
24
+ location [string] :
23
25
 
24
26
  confirm_throw_fires_on_exception [test] :
25
27
  description [string] : Confirm throw fires on_exception and populates exception_data, but does not fire on_error.
@@ -59,3 +61,61 @@ tests.lang.exceptions [can] :
59
61
 
60
62
  eval ^^.exc_fired = false
61
63
  assert "expected on_exception to NOT have fired for a gloo-level error"
64
+
65
+ confirm_missing_value_fires_on_error [test] :
66
+ description [string] : Using a missing object as a value is a runtime error, but asking whether it exists is not.
67
+ on_test [script] :
68
+ put false into ^^.err_fired
69
+ exists? instance ^^.no_such_value
70
+ eval ^^.err_fired = false
71
+ assert "expected exists? on a missing object to not fire on_error"
72
+
73
+ show ^^.no_such_value
74
+ eval ^^.err_fired = true
75
+ assert "expected using a missing object as a value to fire on_error"
76
+
77
+ eval ^^.error_data.message = "Object '^^.no_such_value' was not found."
78
+ assert "expected error_data.message to name the missing object"
79
+
80
+ kept [string] : keep me
81
+
82
+ confirm_put_of_missing_value_keeps_target [test] :
83
+ description [string] : A put whose value uses a missing object leaves the target unchanged.
84
+ on_test [script] :
85
+ put 'keep me' into ^^.kept
86
+ put ^^.no_such_value into ^^.kept
87
+ eval ^^.kept = 'keep me'
88
+ assert "expected the target to keep its value"
89
+
90
+ confirm_error_kind [test] :
91
+ description [string] : error_data.kind says whether the command couldn't be understood (syntax) or couldn't be done (runtime).
92
+ on_test [script] :
93
+ nosuchverb
94
+ eval ^^.error_data.kind = 'syntax'
95
+ assert "expected an unknown verb to be a syntax error"
96
+
97
+ show ^^.no_such_value
98
+ eval ^^.error_data.kind = 'runtime'
99
+ assert "expected a missing object to be a runtime error"
100
+
101
+ confirm_error_location [test] :
102
+ description [string] : error_data.location names the script and line that failed.
103
+ on_test [script] :
104
+ show 1
105
+ show ^^.no_such_value
106
+ eval ^^.error_data.location = 'tests.lang.exceptions.confirm_error_location.on_test, line 2'
107
+ assert "expected the location of the failed line"
108
+
109
+ num [int] : 5
110
+
111
+ confirm_bad_value_is_a_warning [test] :
112
+ description [string] : A value that isn't really of its type is a warning, not an error; the best guess is used.
113
+ on_test [script] :
114
+ put false into ^^.err_fired
115
+ put 'x' into ^^.num
116
+ eval ^^.err_fired = false
117
+ assert "expected a bad value to not fire on_error"
118
+
119
+ eval ^^.num = 0
120
+ assert "expected the best guess, 0"
121
+
@@ -0,0 +1,139 @@
1
+ #
2
+ # Predictable failures tests
3
+ #
4
+ # A failure a script can cause with ordinary bad input or the outside
5
+ # world (invalid JSON, a template with a mistake, a value that isn't a
6
+ # date) is a gloo runtime error: it fires on_error (not on_exception),
7
+ # reads "Could not ...", and a command that would set it sets it false.
8
+ #
9
+
10
+ tests.lang.failures [can] :
11
+
12
+ err_fired [bool] : false
13
+ exc_fired [bool] : false
14
+
15
+ on_error [script] :
16
+ put true into ^.err_fired
17
+
18
+ on_exception [script] :
19
+ put true into ^.exc_fired
20
+
21
+ error_data [can] :
22
+ message [string] :
23
+ backtrace [string] :
24
+ kind [string] :
25
+ location [string] :
26
+
27
+ j [json] : {bad
28
+ e [erb] :
29
+ template [text] : <%= 1 + %>
30
+ params [can] :
31
+ result [text] :
32
+ d [date] :
33
+ msg [string] :
34
+
35
+ invalid_json_is_an_error [test] :
36
+ description [string] : Pretty-printing invalid JSON fires on_error, not on_exception, and it is false.
37
+ on_test [script] :
38
+ put false into ^^.err_fired
39
+ put false into ^^.exc_fired
40
+ tell ^^.j to pretty
41
+ eval it = false
42
+ assert "expected it to be false"
43
+
44
+ eval ^^.err_fired = true
45
+ assert "expected on_error to fire"
46
+ eval ^^.exc_fired = false
47
+ assert "expected on_exception not to fire"
48
+
49
+ put ^^.error_data.message into ^^.msg
50
+ check ^^.msg for starts_with? ( 'Could not parse the JSON in tests.lang.failures.j:' )
51
+ assert "expected the error to say what couldn't be done"
52
+
53
+ template_syntax_error_is_an_error [test] :
54
+ description [string] : An erb template with a syntax error is a gloo error, and gloo keeps running.
55
+ on_test [script] :
56
+ put false into ^^.err_fired
57
+ put false into ^^.exc_fired
58
+ run ^^.e
59
+ eval it = false
60
+ assert "expected it to be false"
61
+
62
+ eval ^^.err_fired = true
63
+ assert "expected on_error to fire"
64
+ eval ^^.exc_fired = false
65
+ assert "expected on_exception not to fire"
66
+
67
+ not_a_date_is_an_error [test] :
68
+ description [string] : A date message on a value that isn't a date fires on_error, and it is false.
69
+ on_test [script] :
70
+ put '' into ^^.d
71
+ put false into ^^.err_fired
72
+ put false into ^^.exc_fired
73
+ tell ^^.d to mm
74
+ eval it = false
75
+ assert "expected it to be false"
76
+
77
+ eval ^^.err_fired = true
78
+ assert "expected on_error to fire"
79
+ eval ^^.exc_fired = false
80
+ assert "expected on_exception not to fire"
81
+
82
+ unknown_unit_is_an_error [test] :
83
+ description [string] : Adding an amount with an unknown unit fires on_error, and it is false.
84
+ on_test [script] :
85
+ put '2026-01-01' into ^^.d
86
+ put false into ^^.err_fired
87
+ tell ^^.d to add ( '3 dayz' )
88
+ eval it = false
89
+ assert "expected it to be false"
90
+
91
+ eval ^^.err_fired = true
92
+ assert "expected on_error to fire"
93
+
94
+ eval ^^.error_data.message = "'3 dayz' is not an amount like '1 day' or '3 months'."
95
+ assert "expected the error to show what an amount looks like"
96
+
97
+ #
98
+ # Chronic phrases are still dates: the checks above only catch a value
99
+ # that couldn't be read as one.
100
+ #
101
+
102
+ tests.lang.chronic_phrases [can] :
103
+
104
+ d [date] :
105
+ now [date] :
106
+ dt [datetime] :
107
+ a [string] :
108
+ b [string] :
109
+
110
+ today_is_a_date [test] :
111
+ description [string] : put 'today' into a date gives today's date.
112
+ on_test [script] :
113
+ put 'today' into ^^.d
114
+ tell ^^.now to now
115
+ tell ^^.d to format ( '%Y-%m-%d' )
116
+ put it into ^^.a
117
+ tell ^^.now to format ( '%Y-%m-%d' )
118
+ put it into ^^.b
119
+ eval ^^.a = ^^.b
120
+ assert "expected 'today' to be today's date"
121
+
122
+ add_to_a_phrase [test] :
123
+ description [string] : A date set from a phrase can be added to.
124
+ on_test [script] :
125
+ put '2026-01-01' into ^^.d
126
+ tell ^^.d to add ( '1 week' )
127
+ eval it = '2026.01.08'
128
+ assert "expected a week later"
129
+
130
+ tomorrow_is_in_the_future [test] :
131
+ description [string] : 'tomorrow at 3pm' in a datetime is tomorrow, and in the future.
132
+ on_test [script] :
133
+ put 'tomorrow at 3pm' into ^^.dt
134
+ tell ^^.dt to is_tomorrow
135
+ assert "expected it to be tomorrow"
136
+ tell ^^.dt to is_future
137
+ assert "expected it to be in the future"
138
+ tell ^^.dt to is_past
139
+ refute "expected it not to be in the past"
@@ -37,3 +37,136 @@ tests.lang.it [can] :
37
37
  list ^^.n
38
38
  eval it = 42
39
39
  assert "expected it to survive statements that don't produce a result"
40
+
41
+ #
42
+ # it is a read-only result value, not an object: it can be read, but
43
+ # nothing can target it.
44
+ #
45
+
46
+ tests.lang.it_read_only [can] :
47
+
48
+ err_fired [bool] : false
49
+ s [string] :
50
+
51
+ on_error [script] :
52
+ put true into ^.err_fired
53
+
54
+ error_data [can] :
55
+ message [string] :
56
+ backtrace [string] :
57
+ kind [string] :
58
+ location [string] :
59
+
60
+ message_to_it_is_an_error [test] :
61
+ description [string] : Sending it a message reports an error that says what to do instead.
62
+ on_test [script] :
63
+ put false into ^^.err_fired
64
+ eval 'abc'
65
+ check it for starts_with? ( 'a' )
66
+ eval ^^.err_fired = true
67
+ assert "expected check it for ... to be an error"
68
+
69
+ eval ^^.error_data.message = "it isn't an object and can't be sent messages; put it into an object first (put it into x)."
70
+ assert "expected the error to say to put it into an object first"
71
+
72
+ put_into_it_is_an_error [test] :
73
+ description [string] : Putting a value into it is an error.
74
+ on_test [script] :
75
+ put false into ^^.err_fired
76
+ put 'x' into it
77
+ eval ^^.err_fired = true
78
+ assert "expected put into it to be an error"
79
+
80
+ put_it_into_an_object_works [test] :
81
+ description [string] : The workaround: put it into an object, then send that object the message.
82
+ on_test [script] :
83
+ put false into ^^.err_fired
84
+ eval 'abc'
85
+ put it into ^^.s
86
+ check ^^.s for starts_with? ( 'a' )
87
+ assert "expected the message to work on the object"
88
+
89
+ eval ^^.err_fired = false
90
+ assert "expected no error"
91
+
92
+ #
93
+ # The it rule: a command that works out a value puts it in it; when
94
+ # something goes wrong, it is false.
95
+ #
96
+
97
+ tests.lang.it_rule [can] :
98
+
99
+ err_fired [bool] : false
100
+ s [string] :
101
+ kept [string] : keep me
102
+
103
+ on_error [script] :
104
+ put true into ^.err_fired
105
+
106
+ failed_put_sets_it_false [test] :
107
+ description [string] : A put whose value can't be worked out sets it to false and keeps the target.
108
+ on_test [script] :
109
+ put false into ^^.err_fired
110
+ eval 'before'
111
+ put ^^.no_such_value into ^^.kept
112
+ eval it = false
113
+ assert "expected it to be false after a failed put"
114
+
115
+ eval ^^.kept = 'keep me'
116
+ assert "expected the target to keep its value"
117
+
118
+ eval ^^.err_fired = true
119
+ assert "expected the failed put to be an error"
120
+
121
+ put_into_missing_object_sets_it_false [test] :
122
+ description [string] : A put into an object that doesn't exist sets it to false.
123
+ on_test [script] :
124
+ eval 'before'
125
+ put 'x' into ^^.no_such_target
126
+ eval it = false
127
+ assert "expected it to be false"
128
+
129
+ value_messages_set_it [test] :
130
+ description [string] : A message that changes its own value also puts the new value in it.
131
+ on_test [script] :
132
+ put ' padded ' into ^^.s
133
+ tell ^^.s to trim
134
+ eval it = 'padded'
135
+ assert "expected trim to put the trimmed string in it"
136
+
137
+ put 'a b' into ^^.s
138
+ tell ^^.s to format_for_html
139
+ eval it = 'a b'
140
+ assert "expected format_for_html to put the result in it"
141
+
142
+ failed_function_sets_it_false [test] :
143
+ description [string] : A function whose on_invoke fails sets it to false.
144
+ on_test [script] :
145
+ eval 'before'
146
+ invoke ^^.failing
147
+ eval it = false
148
+ assert "expected it to be false after a failed invoke"
149
+
150
+ failing [ƒ] :
151
+ params [can] :
152
+ on_invoke [script] :
153
+ put 1 into no.such.object
154
+ result [int] :
155
+
156
+ message_to_missing_object_sets_it_false [test] :
157
+ description [string] : A message that can't be delivered leaves no stale value in it.
158
+ on_test [script] :
159
+ eval 'stale'
160
+ tell ^^.no_such_obj to get_parent
161
+ eval it = false
162
+ assert "expected it to be false after a message to a missing object"
163
+
164
+ eval 'stale'
165
+ tell ^^.s to no_such_msg
166
+ eval it = false
167
+ assert "expected it to be false after a message the object doesn't take"
168
+
169
+ eval 'stale'
170
+ run ^^.no_such_obj
171
+ eval it = false
172
+ assert "expected it to be false after running a missing object"
@@ -14,6 +14,10 @@ tests.lang.naming [can] :
14
14
  string [string] : a type keyword as a name
15
15
  put [string] : a verb keyword as a name
16
16
 
17
+ # Deliberately has no name: loading reports a syntax error and
18
+ # creates it as unnamed_1.
19
+ [string] : no name given
20
+
17
21
  first_of_a_duplicate_name_wins [test] :
18
22
  description [string] : when two siblings share a name, the path always reaches the first.
19
23
  on_test [script] :
@@ -40,3 +44,10 @@ tests.lang.naming [can] :
40
44
 
41
45
  eval ^^.put = 'a verb keyword as a name'
42
46
  assert "expected a verb keyword to be usable as a name"
47
+
48
+ declaration_with_no_name_gets_a_placeholder [test] :
49
+ description [string] : a declaration with no name loads under the placeholder name unnamed_1.
50
+ on_test [script] :
51
+ eval ^^.unnamed_1 = 'no name given'
52
+ assert "expected the nameless object to be reached as unnamed_1"
53
+
@@ -4,6 +4,10 @@
4
4
 
5
5
  tests.lang.ops [can] :
6
6
 
7
+ first [string] : Ada
8
+ last [string] : Lovelace
9
+ full [string] :
10
+
7
11
  confirm_eq_ops [test] :
8
12
  description [string] : Confirm equality operations.
9
13
  on_test [script] :
@@ -75,3 +79,29 @@ tests.lang.ops [can] :
75
79
 
76
80
  eval 1 <= 1
77
81
  assert "expected 1 <= 1"
82
+
83
+ confirm_and_joins [test] :
84
+ description [string] : and joins values, the same as +.
85
+ on_test [script] :
86
+ put ^^.first and ' ' and ^^.last into ^^.full
87
+ eval ^^.full = 'Ada Lovelace'
88
+ assert "expected first and ' ' and last to be 'Ada Lovelace'"
89
+
90
+ eval 2 and 3 = 5
91
+ assert "expected 2 and 3 to be 5"
92
+
93
+ confirm_join_without_operator [test] :
94
+ description [string] : Values side by side with no operator between them are joined.
95
+ on_test [script] :
96
+ put 'hello ' 'world' into ^^.full
97
+ eval ^^.full = 'hello world'
98
+ assert "expected 'hello ' 'world' to be 'hello world'"
99
+
100
+ put ^^.first ' ' ^^.last into ^^.full
101
+ eval ^^.full = 'Ada Lovelace'
102
+ assert "expected first ' ' last to be 'Ada Lovelace'"
103
+
104
+ put 'a' 'b' + 'c' and 'd' into ^^.full
105
+ eval ^^.full = 'abcd'
106
+ assert "expected no operator, + and and to mix"
107
+
@@ -43,3 +43,104 @@ tests.objs.can [can] :
43
43
  eval it = 0
44
44
  assert "expected it to be 0"
45
45
 
46
+
47
+ #
48
+ # Child value and path messages.
49
+ #
50
+
51
+ tests.objs.can_child [can] :
52
+
53
+ list [can] :
54
+ x [string] : ex
55
+ y [integer] : 7
56
+ book [can] :
57
+ title [string] : Walden
58
+
59
+ empty [can] :
60
+ path [string] :
61
+ ptr [alias] :
62
+ slot [alias] :
63
+ idx [integer] : 0
64
+ n [integer] : 0
65
+ found [integer] : 0
66
+ picked [can] :
67
+
68
+ # Pick distinct random children: a random index, skipped if already used.
69
+ pick_distinct [repeat] :
70
+ times [integer] : 200
71
+ index [integer] : 0
72
+ do [script] :
73
+ tell tests.objs.can_child.idx to randomize ( tests.objs.can_child.n )
74
+ check tests.objs.can_child.picked for child_exists ( tests.objs.can_child.idx )
75
+ unless it do tell tests.objs.can_child.mark to run
76
+
77
+ mark [script] :
78
+ put 'tests.objs.can_child.picked.' + tests.objs.can_child.idx into tests.objs.can_child.slot*
79
+ create tests.objs.can_child.slot* as integer
80
+ put tests.objs.can_child.found + 1 into tests.objs.can_child.found
81
+
82
+ value_at [test] :
83
+ description [string] : child_value_at gets a simple child's value by 0-based index.
84
+ on_test [script] :
85
+ tell ^^.list to child_value_at ( 0 )
86
+ eval it = 'ex'
87
+ assert "expected index 0 to be the first child"
88
+
89
+ tell ^^.list to child_value_at ( 1 )
90
+ eval it = 7
91
+ assert "expected index 1 to be the second child"
92
+
93
+ value_at_container_child [test] :
94
+ description [string] : child_value_at on a container child is an error, and it is false.
95
+ on_test [script] :
96
+ tell ^^.list to child_value_at ( 2 )
97
+ eval it = false
98
+ assert "expected false for a container child"
99
+
100
+ path_at_alias [test] :
101
+ description [string] : child_path_at into an alias reaches a container child's fields.
102
+ on_test [script] :
103
+ tell ^^.list to child_path_at ( 2 )
104
+ put it into ^^.path
105
+ eval ^^.path = 'tests.objs.can_child.list.book'
106
+ assert "expected the path from root"
107
+
108
+ put ^^.path into ^^.ptr*
109
+ eval ^^.ptr.title = 'Walden'
110
+ assert "expected to read the field through the alias"
111
+
112
+ out_of_range [test] :
113
+ description [string] : An out-of-range index is an error, and it is false.
114
+ on_test [script] :
115
+ tell ^^.list to child_path_at ( 3 )
116
+ eval it = false
117
+ assert "expected false for index 3 of 3 children"
118
+
119
+ random_path [test] :
120
+ description [string] : random_child_path gives the path of one of the children.
121
+ on_test [script] :
122
+ tell ^^.list to random_child_path
123
+ put it into ^^.path
124
+ check ^^.path for starts_with? ( 'tests.objs.can_child.list.' )
125
+ assert "expected a child's path"
126
+
127
+ random_empty [test] :
128
+ description [string] : random_child_value on an empty container is an error, and it is false.
129
+ on_test [script] :
130
+ tell ^^.empty to random_child_value
131
+ eval it = false
132
+ assert "expected false for an empty container"
133
+
134
+ distinct_random_picks [test] :
135
+ description [string] : A random index plus a picked container gives distinct picks.
136
+ on_test [script] :
137
+ tell ^^.list to count
138
+ put it into ^^.n
139
+ tell ^^.pick_distinct to run
140
+
141
+ tell ^^.picked to count
142
+ eval it = 3
143
+ assert "expected every child picked exactly once"
144
+
145
+ eval ^^.found = 3
146
+ assert "expected no index to be picked twice"
@@ -52,6 +52,30 @@ tests.objs.string [can] :
52
52
  eval ^^.s = 'one two three'
53
53
  assert "expected split to leave the string unchanged"
54
54
 
55
+ index_of [test] :
56
+ description [string] : Find the position of a substring
57
+ on_test [script] :
58
+ put 'one two three' into ^^.s
59
+ tell ^^.s to index_of ('two')
60
+ eval it = 4
61
+ assert "expected index_of ('two') to equal 4"
62
+ tell ^^.s to index_of ('four')
63
+ eval it = -1
64
+ assert "expected index_of a missing needle to equal -1"
65
+ eval ^^.s = 'one two three'
66
+ assert "expected index_of to leave the string unchanged"
67
+
68
+ index_of_from [test] :
69
+ description [string] : Find a substring at or after a start position
70
+ on_test [script] :
71
+ put '![[123.png]] and ]]' into ^^.s
72
+ tell ^^.s to index_of (']]' 3)
73
+ eval it = 10
74
+ assert "expected index_of (']]' 3) to equal 10"
75
+ tell ^^.s to index_of (']]' 11)
76
+ eval it = 17
77
+ assert "expected index_of (']]' 11) to equal 17"
78
+
55
79
  splitl [test] :
56
80
  description [string] : Get the substring to the left of an index
57
81
  on_test [script] :