)" }, -- "a = function(arg1, ..., argN)"
69 | { true, fixup "break" }, -- "break" generates no trace in Lua 5.2
70 | { true, "{" }, -- "{" opening table
71 | { true, "}" }, -- "{" closing table
72 | { true, fixup "})" }, -- function closer
73 | { true, fixup ")" }, -- function closer
74 | }
75 |
76 | ------------------------
77 | -- Starts the report generator
78 | -- To load a config, use luacov.runner to load
79 | -- settings and then start the report.
80 | -- @example# local runner = require("luacov.runner")
81 | -- local reporter = require("luacov.reporter")
82 | -- runner.load_config()
83 | -- table.insert(luacov.configuration.include, "thisfile")
84 | -- reporter.report()
85 | function M.report()
86 | local luacov = require("luacov.runner")
87 | local stats = require("luacov.stats")
88 |
89 | local configuration = luacov.load_config()
90 | stats.statsfile = configuration.statsfile
91 |
92 | local data, most_hits = stats.load()
93 |
94 | if not data then
95 | print("Could not load stats file "..configuration.statsfile..".")
96 | print("Run your Lua program with -lluacov and then rerun luacov.")
97 | os.exit(1)
98 | end
99 |
100 | local report = io.open(configuration.reportfile, "w")
101 |
102 | local names = {}
103 | for filename, _ in pairs(data) do
104 | local include = false
105 | -- normalize paths in patterns
106 | local path = filename:gsub("/", "."):gsub("\\", "."):gsub("%.lua$", "")
107 | if not configuration.include[1] then
108 | include = true
109 | else
110 | include = false
111 | for _, p in ipairs(configuration.include) do
112 | if path:match(p) then
113 | include = true
114 | break
115 | end
116 | end
117 | end
118 | if include and configuration.exclude[1] then
119 | for _, p in ipairs(configuration.exclude) do
120 | if path:match(p) then
121 | include = false
122 | break
123 | end
124 | end
125 | end
126 | if include then
127 | table.insert(names, filename)
128 | end
129 | end
130 |
131 | table.sort(names)
132 |
133 | local summary = {}
134 | local most_hits_length = ("%d"):format(most_hits):len()
135 | local empty_format = (" "):rep(most_hits_length+1)
136 | local zero_format = ("*"):rep(most_hits_length).."0"
137 | local count_format = ("%% %dd"):format(most_hits_length+1)
138 |
139 | local function excluded(exclusions,line)
140 | for _, e in ipairs(exclusions) do
141 | if e[1] then
142 | if line:match("^ *"..e[2].." *$") or line:match("^ *"..e[2].." *%-%-") then return true end
143 | else
144 | if line:match(e[2]) then return true end
145 | end
146 | end
147 | return false
148 | end
149 |
150 | for _, filename in ipairs(names) do
151 | local filedata = data[filename]
152 | local file = io.open(filename, "r")
153 | if file then
154 | report:write("\n")
155 | report:write("==============================================================================\n")
156 | report:write(filename, "\n")
157 | report:write("==============================================================================\n")
158 | local line_nr = 1
159 | local file_hits, file_miss = 0, 0
160 | local block_comment, equals = false, ""
161 | local in_long_string, ls_equals = false, ""
162 | while true do
163 | local line = file:read("*l")
164 | if not line then break end
165 | local true_line = line
166 |
167 | local new_block_comment = false
168 | if not block_comment then
169 | line = line:gsub("%s+", " ")
170 | local l, equals = line:match("^(.*)%-%-%[(=*)%[")
171 | if l then
172 | line = l
173 | new_block_comment = true
174 | end
175 | in_long_string, ls_equals = check_long_string(line, in_long_string, ls_equals, filedata[line_nr])
176 | else
177 | local l = line:match("%]"..equals.."%](.*)$")
178 | if l then
179 | line = l
180 | block_comment = false
181 | end
182 | end
183 |
184 | local hits = filedata[line_nr] or 0
185 | if block_comment or in_long_string or excluded(exclusions,line) or (hits == 0 and excluded(hit0_exclusions,line)) then
186 | report:write(empty_format)
187 | else
188 | if hits == 0 then
189 | file_miss = file_miss + 1
190 | report:write(zero_format)
191 | else
192 | file_hits = file_hits + 1
193 | report:write(count_format:format(hits))
194 | end
195 | end
196 | report:write("\t", true_line, "\n")
197 | if new_block_comment then block_comment = true end
198 | line_nr = line_nr + 1
199 | summary[filename] = {
200 | hits = file_hits,
201 | miss = file_miss
202 | }
203 | end
204 | file:close()
205 | end
206 | end
207 |
208 | report:write("\n")
209 | report:write("==============================================================================\n")
210 | report:write("Summary\n")
211 | report:write("==============================================================================\n")
212 | report:write("\n")
213 |
214 | local function write_total(hits, miss, filename)
215 | report:write(hits, "\t", miss, "\t", ("%.2f%%"):format(hits/(hits+miss)*100.0), "\t", filename, "\n")
216 | end
217 |
218 | local total_hits, total_miss = 0, 0
219 | for _, filename in ipairs(names) do
220 | local s = summary[filename]
221 | if s then
222 | write_total(s.hits, s.miss, filename)
223 | total_hits = total_hits + s.hits
224 | total_miss = total_miss + s.miss
225 | end
226 | end
227 | report:write("------------------------\n")
228 | write_total(total_hits, total_miss, "")
229 |
230 | report:close()
231 |
232 | if configuration.deletestats then
233 | os.remove(configuration.statsfile)
234 | end
235 | end
236 |
237 | return M
238 |
--------------------------------------------------------------------------------
/test/scxml-suite/run_suite.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | #encoding: utf-8
3 | BASE = 'http://www.w3.org/Voice/2013/SCXML-irp/'
4 | CACHE = 'spec-cache'
5 | REPORT = 'scxml10-ir-results-lxsc.xml'
6 |
7 | require 'uri'
8 | require 'fileutils'
9 | require 'nokogiri' # gem install nokogiri
10 |
11 | def run!
12 | Dir.chdir(File.dirname(__FILE__)) do
13 | FileUtils.mkdir_p CACHE
14 | @manifest = Nokogiri.XML( get_file('manifest.xml'), &:noblanks )
15 | @mod = Nokogiri.XML(IO.read('manifest-mod.xml'))
16 | @report = Nokogiri.XML(IO.read(REPORT),&:noblanks)
17 | run_tests
18 | File.open(REPORT,'w'){ |f| f<<@report }
19 | end
20 | end
21 |
22 | def run_tests
23 | Dir['*.scxml'].each{ |f| File.delete(f) }
24 | Dir['*.txml' ].each{ |f| File.delete(f) }
25 | FileUtils.rm_f('luacov.stats.out')
26 | @report.xpath('//assert').remove
27 |
28 | tests = @manifest.xpath('//test')
29 | required = tests.reject{ |t| t['conformance']=='optional' }
30 | required.sort_by{ |test| [test['manual']=='false' ? 0 : 1,test['id']] }.each.with_index do |test,i|
31 | id = test['id']
32 | auto = test['manual']=='false'
33 | start = test.at('start')
34 | uri = start['uri']
35 |
36 | print "Test ##{i+1}/#{required.length} #{uri} (#{auto ? :auto : :manual}): "
37 |
38 | scxml = prepare_scxml(uri)
39 | # Fetch dependent files, copy to working directory
40 | test.xpath('dep').each{ |d| File.open(File.basename(d['uri']),'w'){ |f| f<"
56 | # Destroy local copies of dependent files
57 | test.xpath('dep').each{ |d| FileUtils.rm_f(File.basename d['uri']) }
58 | FileUtils.rm_f(scxml)
59 | puts "pass"
60 | else
61 | @report.root << ""
62 | `subl #{scxml}`
63 | puts "fail"
64 | end
65 | else
66 | puts "trace"
67 | system("lua autotest.lua #{scxml} --trace")
68 | end
69 | end
70 | end
71 |
72 | def prepare_scxml(uri)
73 | doc = Nokogiri.XML( get_file(uri), &:noblanks )
74 | convert_to_scxml!(doc)
75 | File.basename(uri).sub('txml','scxml').tap do |file|
76 | File.open(file,'w:utf-8'){ |f| f.puts doc }
77 | end
78 | end
79 |
80 | def convert_to_scxml!(doc)
81 | doc.at_xpath('//conf:pass').replace '' if doc.at_xpath('//conf:pass')
82 | doc.at_xpath('//conf:fail').replace '' if doc.at_xpath('//conf:fail')
83 | {
84 | arrayVar: ->(a){ ['array', "testvar#{a}" ]},
85 | arrayTextVar: ->(a){ ['array', "testvar#{a}" ]},
86 | eventdataVal: ->(a){ ['cond', "_event.data == #{a}" ]},
87 | eventNameVal: ->(a){ ['cond', "_event.name == '#{a}'" ]},
88 | originTypeEq: ->(a){ ['cond', "_event.origintype == '#{a}'" ]},
89 | emptyEventData: ->(a){ ['cond', "_event.data == nil" ]},
90 | eventFieldHasNoValue: ->(a){ ['cond', "_event.#{a} == ''" ]},
91 | isBound: ->(a){ ['cond', "testvar#{a} ~= nil" ]},
92 | inState: ->(a){ ['cond', "In('#{a}')" ]},
93 | true: ->(a){ ['cond', 'true' ]},
94 | false: ->(a){ ['cond', 'false' ]},
95 | unboundVar: ->(a){ ['cond', "testvar#{a}==nil" ]},
96 | noValue: ->(a){ ['cond', "testvar#{a}==nil or testvar#{a}==''" ]},
97 | nameVarVal: ->(a){ ['cond', "_name == '#{a}'" ]},
98 | nonBoolean: ->(a){ ['cond', "@@@@@@@@@@@@@@@@" ]},
99 | systemVarIsBound: ->(a){ ['cond', "#{a} ~= nil" ]},
100 | varPrefix: ->(a){
101 | x,y = a.split /\s+/
102 | ['cond',"string.sub(testvar#{y},1,string.len(testvar#{x}))==testvar#{x}"]
103 | },
104 | VarEqVar: ->(a){
105 | x,y = a.split /\s+/
106 | ['cond',"testvar#{x}==testvar#{y}"]
107 | },
108 | idQuoteVal: ->(a){
109 | x,op,y = a.split(/([=<>]=?)/)
110 | ['cond',"testvar#{x} #{op=='=' ? '==' : op} '#{y}'"]
111 | },
112 | idVal: ->(a){
113 | x,op,y = a.split /([=<>]+)/
114 | ['cond',"testvar#{x} #{op == '=' ? '==' : op} #{y}"]
115 | },
116 | namelistIdVal: ->(a){
117 | x,op,y = a.split /([=<>]+)/
118 | ['cond',"testvar#{x} #{op == '=' ? '==' : op} #{y}"]
119 | },
120 | idSystemVarVal: ->(a){
121 | x,op,y = a.split /([=<>]+)/
122 | ['cond',"testvar#{x} #{op == '=' ? '==' : op} #{y}"]
123 | },
124 | compareIDVal: ->(a){
125 | x,op,y = a.split /([=<>]+)/
126 | ['cond',"testvar#{x} #{op == '=' ? '==' : op} testvar#{y}"]
127 | },
128 | eventvarVal: ->(a){
129 | x,op,y = a.split /([=<>]+)/
130 | ['cond',"_event.data['testvar#{x}'] #{op == '=' ? '==' : op} #{y}"]
131 | },
132 | VarEqVarStruct: ->(a){
133 | x,y = a.split /\D+/
134 | ['cond',"testvar#{x} == testvar#{y}"]
135 | },
136 | eventFieldsAreBound: ->(a){
137 | ['cond', "_event.name~=nil and _event.type~=nil and _event.sendid~=nil and _event.origin~=nil and _event.invokeid~=nil"]
138 | },
139 | datamodel: ->(a){ ['datamodel', 'lua' ]},
140 | delay: ->(a){ ['delay', "#{100*a.to_i}ms" ]},
141 | delayExpr: ->(a){ ['delayexpr', "testvar#{a}" ]},
142 | delayFromVar: ->(a){ ['delayexpr', "testvar#{a}" ]},
143 | # delayFromVar: ->(a){ ['delayexpr', "100*tonumber(testvar#{a})..'ms'" ]},
144 | eventExpr: ->(a){ ['eventexpr', "testvar#{a}" ]},
145 | eventDataFieldValue: ->(a){ ['expr', "_event.data.#{a}" ]},
146 | eventDataNamelistValue: ->(a){ ['expr', "_event.data.testvar#{a}" ]},
147 | eventDataParamValue: ->(a){ ['expr', "_event.data.#{a}" ]},
148 | eventField: ->(a){ ['expr', "_event.#{a}" ]},
149 | eventName: ->(a){ ['expr', "_event.name" ]},
150 | eventSendid: ->(a){ ['expr', "_event.sendid" ]},
151 | eventType: ->(a){ ['expr', "_event.type" ]},
152 | eventRaw: ->(a){ ['expr', "_event:inspect(true)"]},
153 | expr: ->(a){ ['expr', a ]},
154 | illegalArray: ->(a){ ['expr', "7" ]},
155 | illegalExpr: ->(a){ ['expr', "!" ]},
156 | invalidSendTypeExpr: ->(a){ ['expr', '27' ]},
157 | invalidSessionID: ->(a){ ['expr', "-1" ]},
158 | invalidName: ->(a){ ['name', "" ]},
159 | varExpr: ->(a){ ['expr', "testvar#{a}" ]},
160 | varChildExpr: ->(a){ ['expr', "testvar#{a}" ]},
161 | quoteExpr: ->(a){ ['expr', "'#{a}'" ]},
162 | systemVarExpr: ->(a){ ['expr', a ]},
163 | scxmlEventIOLocation: ->(a){ ['expr', "FIXME" ]},
164 | varNonexistentStruct: ->(a){ ['expr', "testvar#{a}.nonono" ]},
165 | id: ->(a){ ['id', "testvar#{a}" ]},
166 | idlocation: ->(a){ ['idlocation', "'testvar#{a}'" ]},
167 | index: ->(a){ ['index', "testvar#{a}" ]},
168 | item: ->(a){ ['item', "testvar#{a}" ]},
169 | illegalItem: ->(a){ ['item', "_no" ]},
170 | location: ->(a){ ['location', "testvar#{a}" ]},
171 | invalidLocation: ->(a){ ['location', "" ]},
172 | invalidParamLocation: ->(a){ ['location', "" ]},
173 | systemVarLocation: ->(a){ ['location', a ]},
174 | name: ->(a){ ['name', "testvar#{a}" ]},
175 | namelist: ->(a){ ['namelist', "testvar#{a}" ]},
176 | invalidNamelist: ->(a){ ['namelist', "" ]},
177 | sendIDExpr: ->(a){ ['sendidexpr', "testvar#{a}" ]},
178 | srcExpr: ->(a){ ['srcexpr', "testvar#{a}" ]},
179 | scriptBadSrc: ->(a){ ['src', "-badfile-" ]},
180 | targetpass: ->(a){ ['target', 'pass' ]},
181 | targetfail: ->(a){ ['target', 'fail' ]},
182 | illegalTarget: ->(a){ ['target', 'xxxxxxxxx' ]},
183 | unreachableTarget: ->(a){ ['target', 'FIXME' ]},
184 | targetVar: ->(a){ ['targetexpr', "testvar#{a}" ]},
185 | targetExpr: ->(a){ ['targetexpr', "testvar#{a}" ]},
186 | basicHTTPAccessURITarget: ->(a){ ['targetexpr', "FIXME" ]},
187 | invalidSendType: ->(a){ ['type', '27' ]},
188 | typeExpr: ->(a){ ['typeexpr', "testvar#{a}" ]},
189 | }.each do |a1,proc|
190 | doc.xpath("//@conf:#{a1}").each{ |a| a2,v=proc[a.value]; a.parent[a2]=v; a.remove }
191 | end
192 |
193 | doc.xpath('//conf:incrementID').each{ |e|
194 | e.replace ""
195 | }
196 | doc.xpath('//conf:array123').each{ |e| e.replace "{1,2,3}" }
197 | doc.xpath('//conf:extendArray').each{ |e| e.replace "" }
198 | doc.xpath('//conf:sumVars').each{ |e|
199 | e.replace ""
200 | }
201 | doc.xpath('//conf:concatVars').each{ |e|
202 | e.replace ""
203 | }
204 | doc.xpath('//conf:contentFoo').each{ |e| e.replace %Q{} }
205 | doc.xpath('//conf:script').each{ |e| e.replace %Q{} }
206 | doc.xpath('//conf:sendToSender').each{ |e|
207 | e.replace %Q{}
208 | }
209 |
210 | if a = doc.at_xpath('//@*[namespace-uri()="http://www.w3.org/2005/scxml-conformance"]')
211 | puts a.parent
212 | exit
213 | end
214 | if a = doc.at_xpath('//conf:*')
215 | puts a
216 | exit
217 | end
218 |
219 | # HACK to remove the now-unused conf: namespace from the root.
220 | doc.remove_namespaces!
221 | doc.root.add_namespace(nil,'http://www.w3.org/2005/07/scxml')
222 | end
223 |
224 | def get_file(uri)
225 | Dir.chdir(CACHE) do
226 | unless File.exist?(uri)
227 | subdir = File.dirname(uri)
228 | FileUtils.mkdir_p subdir
229 | Dir.chdir(subdir){ `curl -s -L -O #{URI.join BASE, uri}` }
230 | end
231 | File.open( uri, 'r:UTF-8', &:read )
232 | end
233 | end
234 |
235 | run! if __FILE__==$0
--------------------------------------------------------------------------------
/test/test.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env lua
2 | package.path = "../?.lua;" .. package.path
3 | require 'io'
4 |
5 | _ENV = require('lunity')('LXSC Tests')
6 |
7 | local LXSC = require 'lxsc'
8 |
9 | DIR = 'testcases'
10 | SHOULD_NOT_FINISH = {final2=true}
11 |
12 | XML = {}
13 | for filename in io.popen(string.format('ls "%s"',DIR)):lines() do
14 | local testName = filename:sub(1,-7)
15 | XML[testName] = io.open(DIR.."/"..filename):read("*all")
16 | end
17 |
18 | function test:parsing()
19 | local m = LXSC:parse(XML['internal_transition'])
20 | -- assertNil(m.id,"The scxml should not have an id")
21 | assertTrue(m.isCompound,'The root state should be compound')
22 | assertEqual(m.states[1].id,'outer')
23 | assertEqual(m.states[2].id,'fail')
24 | assertEqual(m.states[3].id,'pass')
25 | assertEqual(#m.states,3,"internal_transition.scxml should have 3 root states")
26 | local outer = m.states[1]
27 | assertEqual(#outer.states,2,"There should be 2 child states of the 'outer' state")
28 | assertEqual(#outer._onexits,1,"There should be 1 onexit command for the 'outer' state")
29 | assertEqual(#outer._onentrys,0,"There should be 0 onentry commands for the 'outer' state")
30 |
31 | m = LXSC:parse(XML['history'])
32 | assertSameKeys(m:allStateIds(),{["wrap"]=1,["universe"]=1,["history-actions"]=1,["action-1"]=1,["action-2"]=1,["action-3"]=1,["action-4"]=1,["modal-dialog"]=1,["pass"]=1,["fail"]=1})
33 | assertSameKeys(m:atomicStateIds(),{["history-actions"]=1,["action-1"]=1,["action-2"]=1,["action-3"]=1,["action-4"]=1,["modal-dialog"]=1,["pass"]=1,["fail"]=1})
34 |
35 | m = LXSC:parse(XML['parallel4'])
36 | assertSameKeys(m:allStateIds(),{["wrap"]=1,["p"]=1,["a"]=1,["a1"]=1,["a2"]=1,["b"]=1,["b1"]=1,["b2"]=1,["pass"]=1})
37 | assertSameKeys(m:atomicStateIds(),{["a1"]=1,["a2"]=1,["b1"]=1,["b2"]=1,["pass"]=1})
38 | end
39 |
40 | function test:dataAccess()
41 | local s = LXSC:parse[[
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | ]]
50 |
51 | s:start()
52 | assert(s:isActive('errord'),"There should be an error when boot() can't be found")
53 |
54 | s:start{ data={ boot=function() end } }
55 | assert(s:isActive('s'),"There should be no error when boot() is supplied")
56 |
57 | -- s:start{ data={ boot=function() n=7 end } }
58 | -- assert(s:isActive('pass'),"Setting 'global' variables populates data model")
59 |
60 | s:start{ data={ boot=function() end, m=42 } }
61 | assertEqual(s:get("m"),42,"The data model should accept initial values")
62 |
63 | s:set("foo","bar")
64 | s:set("jim",false)
65 | s:set("n",6)
66 | assertEqual(s:get("foo"),"bar")
67 | assertEqual(s:get("jim"),false)
68 | assertEqual(s:get("n")*7,42)
69 |
70 | s:start()
71 | assertNil(s:get("boot"),"Starting the machine resets the datamodel")
72 | assertNil(s:get("foo"),"Starting the machine resets the datamodel")
73 |
74 | s:start{ data={ boot=function() end, n=6 } }
75 | assert(s:isActive('s'))
76 | s:set("n",7)
77 | assert(s:isActive('s'))
78 | s:step()
79 | assert(s:isActive('pass'))
80 |
81 | s:restart()
82 | assert(s:isActive('errord'))
83 |
84 | local s = LXSC:parse[[]]
85 | local values = {}
86 | s.onDataSet = function(name,value) values[name]=value end
87 | s:start()
88 | assertNil(values.foo)
89 | s:set("foo",42)
90 | assertEqual(values.foo,42)
91 | end
92 |
93 | function test:eventlist()
94 | local m = LXSC:parse[[
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | ]]
105 | local possible = m:allEvents()
106 | local expected = {["a"]=1,["b.c"]=1,["d.e.f"]=1,["g"]=1,["h"]=1,["x"]=1,["y.z"]=1}
107 | assertSameKeys(possible,expected)
108 |
109 | assertNil(next(m:availableEvents()),"There should be no events before the machine has started.")
110 | m:start()
111 |
112 | local available = m:availableEvents()
113 | local expected = {["a"]=1,["b.c"]=1,["d.e.f"]=1,["g"]=1,["h"]=1}
114 | assertSameKeys(available,expected)
115 | end
116 |
117 | function test:customHandlers()
118 | local s = LXSC:parse[[
119 |
121 |
122 |
123 | ]]
124 | local goSeen = {}
125 | function LXSC.Exec:go() goSeen[self._nsURI] = true; return true end
126 | assertNil(goSeen.foo)
127 | assertNil(goSeen.bar)
128 | s:start()
129 | assertTrue(goSeen.foo)
130 | assertTrue(goSeen.bar)
131 | end
132 |
133 | function test:customCallbacks()
134 | local s = LXSC:parse[[
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | ]]
146 | local callbackCountById, eventsSeen = {}, {}
147 | local changesSeen = 0
148 | s.onAfterEnter = function(id,kind,atomic)
149 | assertType(id,'string')
150 | if not callbackCountById[id] then callbackCountById[id] = {} end
151 | callbackCountById[id].enter = (callbackCountById[id].enter or 0 ) + 1
152 | if id=='s1' or id=='s2' then
153 | assertEqual(kind,'state')
154 | assertTrue(atomic)
155 | elseif id=='s3' then
156 | assertEqual(kind,'final')
157 | assertTrue(atomic)
158 | else
159 | assertFalse(atomic)
160 | end
161 | end
162 | s.onBeforeExit = function(id,kind,atomic)
163 | assertType(id,'string')
164 | if not callbackCountById[id] then callbackCountById[id] = {} end
165 | callbackCountById[id].exit = (callbackCountById[id].exit or 0 ) + 1
166 | if id=='s1' or id=='s2' then
167 | assertEqual(kind,'state')
168 | assertTrue(atomic)
169 | elseif id=='s3' then
170 | assertEqual(kind,'final')
171 | assertTrue(atomic)
172 | else
173 | assertFalse(atomic)
174 | end
175 | end
176 | s.onEventFired = function(event)
177 | eventsSeen[event.name] = true
178 | end
179 | s.onEnteredAll = function() changesSeen = changesSeen+1 end
180 | s:start()
181 | for id,counts in pairs(callbackCountById) do
182 | if id=='s3' then
183 | assertEqual(counts.enter,1)
184 | assertNil(counts.exit, 0)
185 | else
186 | assertEqual(counts.enter,1)
187 | assertEqual(counts.exit, 1)
188 | end
189 | end
190 | s:fireEvent("foo.bar")
191 | assert(eventsSeen.e)
192 | assert(eventsSeen["foo.bar"])
193 | assertEqual(changesSeen,1)
194 | end
195 |
196 | function test:delayedSend()
197 | local s = LXSC:parse[[
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 | ]]
222 | local elapsed = 0
223 | function s:elapsed() return elapsed end
224 | s:start()
225 | assert(s:isActive('s2'))
226 | s:step()
227 | assert(s:isActive('s2'))
228 | s:cancelDelayedSend('killme')
229 | elapsed = 0.5
230 | s:step()
231 | assert(s:isActive('pass'))
232 | end
233 |
234 | function test:eventMatching()
235 | local descriptors = {
236 | ["*"] = {
237 | shouldMatch={"a","a.b","b.c","b.c.d","c.d.e","c.d.e.f","d.e.f","d.e.f.g","f","f.g","alpha","b.charlie","d.e.frank","frank","b","z.a"},
238 | shouldNotMatch={} },
239 | ["a"] = {
240 | shouldMatch={"a","a.b"},
241 | shouldNotMatch={"b.c","b.c.d","c.d.e","c.d.e.f","d.e.f","d.e.f.g","f","f.g","alpha","b.charlie","d.e.frank","frank","b","z.a"} },
242 | ["b.c"] = {
243 | shouldMatch={"b.c","b.c.d"},
244 | shouldNotMatch={"a","a.b","alpha","b.charlie","d.e.frank","frank","b","z.a","c.d.e","c.d.e.f","d.e.f","d.e.f.g","f","f.g"} },
245 | ["c.d.e"] = {
246 | shouldMatch={"c.d.e","c.d.e.f"},
247 | shouldNotMatch={"a","a.b","b.c","b.c.d","alpha","b.charlie","d.e.frank","frank","b","z.a","d.e.f","d.e.f.g","f","f.g"} },
248 | ["d.e.f.*"] = {
249 | shouldMatch={"d.e.f","d.e.f.g"},
250 | shouldNotMatch={"a","a.b","b.c","b.c.d","c.d.e","c.d.e.f","alpha","b.charlie","d.e.frank","frank","b","z.a","f","f.g"} },
251 | ["f."] = {
252 | shouldMatch={"f","f.g"},
253 | shouldNotMatch={"a","a.b","b.c","b.c.d","c.d.e","c.d.e.f","d.e.f","d.e.f.g","alpha","b.charlie","d.e.frank","frank","b","z.a"} },
254 | }
255 | for descriptor,events in pairs(descriptors) do
256 | local t = LXSC:transition()
257 | t:attr('event',descriptor)
258 |
259 | for _,eventName in ipairs(events.shouldMatch) do
260 | local event = LXSC.Event(eventName,nil,{})
261 | assertTrue(event:triggersDescriptor(descriptor))
262 | assertTrue(event:triggersTransition(t))
263 | end
264 | for _,eventName in ipairs(events.shouldNotMatch) do
265 | local event = LXSC.Event(eventName,nil,{})
266 | assertTrue(not event:triggersDescriptor(descriptor))
267 | assertTrue(not event:triggersTransition(t))
268 | end
269 | end
270 | end
271 |
272 | function test:eval()
273 | local m = LXSC:parse[[
274 |
275 |
276 |
277 |
278 | ]]
279 | m:start()
280 | assertEqual(m:get('a'),1)
281 | assertEqual(m:eval('a'),1)
282 | m:set('a',2)
283 | assertEqual(m:get('a'),2)
284 | assertEqual(m:eval('a'),2)
285 | m:run('a = 3')
286 | assertEqual(m:get('a'),3)
287 | assertEqual(m:eval('a'),3)
288 |
289 | m = LXSC:parse[[
290 |
291 | ]]
292 | local d = {a=1}
293 | m:start{ data=d }
294 | assertEqual(m:get('a'),1)
295 | assertEqual(m:eval('a'),1)
296 | m:set('a',2)
297 | assertEqual(m:get('a'),2)
298 | assertEqual(m:eval('a'),2)
299 | assertEqual(d.a,2)
300 | m:run('a = 3')
301 | assertEqual(m:get('a'),3)
302 | assertEqual(m:eval('a'),3)
303 | assertEqual(d.a,3)
304 | end
305 |
306 | for testName,xml in pairs(XML) do
307 | test["testcase_"..testName] = function()
308 | local machine = LXSC:parse(xml)
309 | assertFalse(machine.running, testName.." should not be running before starting.")
310 | assertTableEmpty(machine:activeStateIds(), testName.." should be empty before running.")
311 | machine:start()
312 | local steps = 0
313 | while steps<1000 and (#machine._internalQueue>0 or #machine._externalQueue>0) do
314 | machine:step()
315 | steps = steps + 1
316 | end
317 | assert(machine:activeStateIds().pass, testName.." should finish in the 'pass' state.")
318 | assertEqual(#machine:activeAtomicIds(), 1, testName.." should only have a single atomic state active.")
319 | if SHOULD_NOT_FINISH[testName] then
320 | assertTrue(machine.running, testName.." should NOT run to completion.")
321 | else
322 | assertFalse(machine.running, testName.." should run to completion.")
323 | end
324 | end
325 | end
326 |
327 |
328 |
329 | test{ useANSI=false }
330 |
--------------------------------------------------------------------------------
/test/lunity.lua:
--------------------------------------------------------------------------------
1 | --[=========================================================================[
2 | Lunity v0.11 by Gavin Kistner
3 | See http://github.com/Phrogz/Lunity for usage documentation.
4 | Licensed under Creative Commons Attribution 3.0 United States License.
5 | See http://creativecommons.org/licenses/by/3.0/us/ for details.
6 | --]=========================================================================]
7 |
8 | -- FIXME: this will fail if two test suites are running interleaved
9 | local assertsPassed, assertsAttempted
10 | local function assertionSucceeded()
11 | assertsPassed = assertsPassed + 1
12 | io.write('.')
13 | return true
14 | end
15 |
16 | -- This is the table that will be used as the environment for the tests,
17 | -- making assertions available within the file.
18 | local lunity = setmetatable({}, {__index=_G})
19 |
20 | function lunity.fail(msg)
21 | assertsAttempted = assertsAttempted + 1
22 | if not msg then msg = "(test failure)" end
23 | error(msg, 2)
24 | end
25 |
26 | function lunity.assert(testCondition, msg)
27 | assertsAttempted = assertsAttempted + 1
28 | if not testCondition then
29 | if not msg then msg = "assert() failed: value was "..tostring(testCondition) end
30 | error(msg, 2)
31 | end
32 | return assertionSucceeded()
33 | end
34 |
35 | function lunity.assertEqual(actual, expected, msg)
36 | assertsAttempted = assertsAttempted + 1
37 | if actual~=expected then
38 | if not msg then
39 | msg = string.format("assertEqual() failed: expected %s, was %s",
40 | tostring(expected),
41 | tostring(actual)
42 | )
43 | end
44 | error(msg, 2)
45 | end
46 | return assertionSucceeded()
47 | end
48 |
49 | function lunity.assertType(actual, expectedType, msg)
50 | assertsAttempted = assertsAttempted + 1
51 | if type(actual) ~= expectedType then
52 | if not msg then
53 | msg = string.format("assertType() failed: value %s is a %s, expected to be a %s",
54 | tostring(actual),
55 | type(actual),
56 | expectedType
57 | )
58 | end
59 | error(msg, 2)
60 | end
61 | return assertionSucceeded()
62 | end
63 |
64 | function lunity.assertTableEquals(actual, expected, msg, keyPath)
65 | assertsAttempted = assertsAttempted + 1
66 | -- Easy out
67 | if actual == expected then
68 | if not keyPath then
69 | return assertionSucceeded()
70 | else
71 | return true
72 | end
73 | end
74 |
75 | if not keyPath then keyPath = {} end
76 |
77 | if type(actual) ~= 'table' then
78 | if not msg then
79 | msg = "Value passed to assertTableEquals() was not a table."
80 | end
81 | error(msg, 2 + #keyPath)
82 | end
83 |
84 | -- Ensure all keys in t1 match in t2
85 | for key,expectedValue in pairs(expected) do
86 | keyPath[#keyPath+1] = tostring(key)
87 | local actualValue = actual[key]
88 | if type(expectedValue)=='table' then
89 | if type(actualValue)~='table' then
90 | if not msg then
91 | msg = "Tables not equal; expected "..table.concat(keyPath,'.').." to be a table, but was a "..type(actualValue)
92 | end
93 | error(msg, 1 + #keyPath)
94 | elseif expectedValue ~= actualValue then
95 | assertTableEquals(actualValue, expectedValue, msg, keyPath)
96 | end
97 | else
98 | if actualValue ~= expectedValue then
99 | if not msg then
100 | if actualValue == nil then
101 | msg = "Tables not equal; missing key '"..table.concat(keyPath,'.').."'."
102 | else
103 | msg = "Tables not equal; expected '"..table.concat(keyPath,'.').."' to be "..tostring(expectedValue)..", but was "..tostring(actualValue)
104 | end
105 | end
106 | error(msg, 1 + #keyPath)
107 | end
108 | end
109 | keyPath[#keyPath] = nil
110 | end
111 |
112 | -- Ensure actual doesn't have keys that aren't expected
113 | for k,_ in pairs(actual) do
114 | if expected[k] == nil then
115 | if not msg then
116 | msg = "Tables not equal; found unexpected key '"..table.concat(keyPath,'.').."."..tostring(k).."'"
117 | end
118 | error(msg, 2 + #keyPath)
119 | end
120 | end
121 |
122 | return assertionSucceeded()
123 | end
124 |
125 | function lunity.assertNotEqual(actual, expected, msg)
126 | assertsAttempted = assertsAttempted + 1
127 | if actual==expected then
128 | if not msg then
129 | msg = string.format("assertNotEqual() failed: value not allowed to be %s",
130 | tostring(actual)
131 | )
132 | end
133 | error(msg, 2)
134 | end
135 | return assertionSucceeded()
136 | end
137 |
138 | function lunity.assertTrue(actual, msg)
139 | assertsAttempted = assertsAttempted + 1
140 | if actual ~= true then
141 | if not msg then
142 | msg = string.format("assertTrue() failed: value was %s, expected true",
143 | tostring(actual)
144 | )
145 | end
146 | error(msg, 2)
147 | end
148 | return assertionSucceeded()
149 | end
150 |
151 | function lunity.assertFalse(actual, msg)
152 | assertsAttempted = assertsAttempted + 1
153 | if actual ~= false then
154 | if not msg then
155 | msg = string.format("assertFalse() failed: value was %s, expected false",
156 | tostring(actual)
157 | )
158 | end
159 | error(msg, 2)
160 | end
161 | return assertionSucceeded()
162 | end
163 |
164 | function lunity.assertNil(actual, msg)
165 | assertsAttempted = assertsAttempted + 1
166 | if actual ~= nil then
167 | if not msg then
168 | msg = string.format("assertNil() failed: value was %s, expected nil",
169 | tostring(actual)
170 | )
171 | end
172 | error(msg, 2)
173 | end
174 | return assertionSucceeded()
175 | end
176 |
177 | function lunity.assertNotNil(actual, msg)
178 | assertsAttempted = assertsAttempted + 1
179 | if actual == nil then
180 | if not msg then msg = "assertNotNil() failed: value was nil" end
181 | error(msg, 2)
182 | end
183 | return assertionSucceeded()
184 | end
185 |
186 | function lunity.assertTableEmpty(actual, msg)
187 | assertsAttempted = assertsAttempted + 1
188 | if type(actual) ~= "table" then
189 | msg = string.format("assertTableEmpty() failed: expected a table, but got a %s",
190 | type(table)
191 | )
192 | error(msg, 2)
193 | else
194 | local key, value = next(actual)
195 | if key ~= nil then
196 | if not msg then
197 | msg = string.format("assertTableEmpty() failed: table has non-nil key %s=%s",
198 | tostring(key),
199 | tostring(value)
200 | )
201 | end
202 | error(msg, 2)
203 | end
204 | return assertionSucceeded()
205 | end
206 | end
207 |
208 | function lunity.assertTableNotEmpty(actual, msg)
209 | assertsAttempted = assertsAttempted + 1
210 | if type(actual) ~= "table" then
211 | msg = string.format("assertTableNotEmpty() failed: expected a table, but got a %s",
212 | type(actual)
213 | )
214 | error(msg, 2)
215 | else
216 | if next(actual) == nil then
217 | if not msg then
218 | msg = "assertTableNotEmpty() failed: table has no keys"
219 | end
220 | error(msg, 2)
221 | end
222 | return assertionSucceeded()
223 | end
224 | end
225 |
226 | function lunity.assertSameKeys(t1, t2, msg)
227 | assertsAttempted = assertsAttempted + 1
228 | local function bail(k,x,y)
229 | if not msg then msg = string.format("Table #%d has key '%s' not present in table #%d",x,tostring(k),y) end
230 | error(msg, 3)
231 | end
232 | for k,_ in pairs(t1) do if t2[k]==nil then bail(k,1,2) end end
233 | for k,_ in pairs(t2) do if t1[k]==nil then bail(k,2,1) end end
234 | return assertionSucceeded()
235 | end
236 |
237 | -- Ensures that the value is a function OR may be called as one
238 | function lunity.assertInvokable(value, msg)
239 | assertsAttempted = assertsAttempted + 1
240 | local meta = getmetatable(value)
241 | if (type(value) ~= 'function') and not (meta and meta.__call and (type(meta.__call)=='function')) then
242 | if not msg then
243 | msg = string.format("assertInvokable() failed: '%s' can not be called as a function",
244 | tostring(value)
245 | )
246 | end
247 | error(msg, 2)
248 | end
249 | return assertionSucceeded()
250 | end
251 |
252 | function lunity.assertErrors(invokable, ...)
253 | assertInvokable(invokable)
254 | if pcall(invokable,...) then
255 | local msg = string.format("assertErrors() failed: %s did not raise an error",
256 | tostring(invokable)
257 | )
258 | error(msg, 2)
259 | end
260 | return assertionSucceeded()
261 | end
262 |
263 | function lunity.assertDoesNotError(invokable, ...)
264 | assertInvokable(invokable)
265 | if not pcall(invokable,...) then
266 | local msg = string.format("assertDoesNotError() failed: %s raised an error",
267 | tostring(invokable)
268 | )
269 | error(msg, 2)
270 | end
271 | return assertionSucceeded()
272 | end
273 |
274 | function lunity.is_nil(value) return type(value)=='nil' end
275 | function lunity.is_boolean(value) return type(value)=='boolean' end
276 | function lunity.is_number(value) return type(value)=='number' end
277 | function lunity.is_string(value) return type(value)=='string' end
278 | function lunity.is_table(value) return type(value)=='table' end
279 | function lunity.is_function(value) return type(value)=='function' end
280 | function lunity.is_thread(value) return type(value)=='thread' end
281 | function lunity.is_userdata(value) return type(value)=='userdata' end
282 |
283 | local function run(self, opts)
284 | if not opts then opts = {} end
285 | assertsPassed = 0
286 | assertsAttempted = 0
287 |
288 | local useANSI,useHTML = true, false
289 | if opts.useHTML ~= nil then useHTML=opts.useHTML end
290 | if not useHTML and opts.useANSI ~= nil then useANSI=opts.useANSI end
291 |
292 | local suiteName = getmetatable(self).name
293 |
294 | if useHTML then
295 | print(""..suiteName.."
")
296 | else
297 | print(string.rep('=',78))
298 | print(suiteName)
299 | print(string.rep('=',78))
300 | end
301 | io.stdout:flush()
302 |
303 |
304 | local testnames = {}
305 | for name, test in pairs(self) do
306 | if type(test)=='function' and name~='before' and name~='after' then
307 | testnames[#testnames+1]=name
308 | end
309 | end
310 | table.sort(testnames)
311 |
312 |
313 | local startTime = os.clock()
314 | local passed = 0
315 | for _,name in ipairs(testnames) do
316 | local scratchpad = {}
317 | io.write(name..": ")
318 | if self.before then self.before(scratchpad) end
319 | local successFlag, errorMessage = pcall(self[name], scratchpad)
320 | if successFlag then
321 | print("pass")
322 | passed = passed + 1
323 | else
324 | if useANSI then
325 | print("\27[31m\27[1mFAIL!\27[0m")
326 | print("\27[31m"..errorMessage.."\27[0m")
327 | elseif useHTML then
328 | print("FAIL!")
329 | print(""..errorMessage.."")
330 | else
331 | print("FAIL!")
332 | print(errorMessage)
333 | end
334 | end
335 | io.stdout:flush()
336 | if self.after then self.after(scratchpad) end
337 | end
338 | local stopTime = os.clock()
339 | if useHTML then
340 | print("")
341 | else
342 | print(string.rep('-', 78))
343 | end
344 |
345 | print(string.format("%d/%d tests passed (%0.1f%%)",
346 | passed,
347 | #testnames,
348 | 100 * passed / #testnames
349 | ))
350 |
351 | if useHTML then print("
") end
352 |
353 | print(string.format("%d total successful assertion%s (%.1f assertions/second)",
354 | assertsPassed,
355 | assertsPassed == 1 and "" or "s",
356 | assertsAttempted / (stopTime - startTime)
357 | ))
358 |
359 | if not useHTML then print("") end
360 | io.stdout:flush()
361 |
362 | end
363 |
364 | return function(name)
365 | return setmetatable(
366 | {test=setmetatable({}, {__call=run, name=name or '(test suite)'})},
367 | {__index=lunity}
368 | )
369 | end
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # About LXSC
2 |
3 | LXSC stands for "Lua XML StateCharts", and is pronounced _"Lexie"_. The LXSC library allows you to run [SCXML state machines][1] in [Lua][2]. The [Data Model][3] for interpretation is all evaluated Lua, allowing you to write conditionals and data expressions in one of the best scripting languages in the world for embedded integration.
4 |
5 | # Table of Contents
6 |
7 | * [Usage](#usage)
8 | * [The Basics](#the-basics)
9 | * [Customizing the Data Model](#customizing-the-data-model)
10 | * [Callbacks as the Machine Changes](#callbacks-as-the-machine-changes)
11 | * [State Change Callbacks](#state-change-callbacks)
12 | * [Transition Callback](#transition-callback)
13 | * [Data Model Callback](#data-model-callback)
14 | * [Event Fire Callback](#event-fire-callback)
15 | * [Peeking and Poking at the Data Model](#peeking-and-poking-at-the-data-model)
16 | * [Examining the State of the Machine](#examining-the-state-of-the-machine)
17 | * [Customizing the Timer](#customizing-the-timer)
18 | * [Custom Executable Content](#custom-executable-content)
19 | * [SCXML Compliance](#scxml-compliance)
20 | * [TODO (aka Known Limitations)](#todo-aka-known-limitations)
21 | * [License & Contact](#license--contact)
22 |
23 | ## Usage
24 |
25 | ### The Basics
26 |
27 | ```lua
28 | local LXSC = require"lxsc-min-12"
29 |
30 | local scxml = io.open('my.scxml'):read('*all')
31 | local machine = LXSC:parse(scxml)
32 | machine:start() -- initiate the interpreter and run until stable
33 |
34 | machine:fireEvent("my.event") -- add events to the event queue to be processed
35 | machine:fireEvent("another.event.name") -- as many as you like; they won't have any effect until you
36 | machine:step() -- call step() to process all events and run until stable
37 |
38 | print("Is the machine still running?",machine.running)
39 | print("Is a state in the configuration?",machine:isActive('some-state-id'))
40 |
41 | -- Keep firing events and calling step() to process them
42 | ```
43 |
44 | ### Customizing the Data Model
45 |
46 | The data model used by the interpreter is a Lua table. This table is used to store and retrieve the values created via [``](http://www.w3.org/TR/scxml/#data) or [``](http://www.w3.org/TR/scxml/#assign). This table is also used as the environment under which the [`` semantic callbacks
52 | * create a custom datatable that performs metamagic when new keys are accessed or modified by the state machine
53 |
54 | You supply a custom data model table by passing a named `data` parameter to the `start()` method:
55 |
56 | ```lua
57 | local mydata = { reloading=true, userName="Gavin" } -- populate initial data values
58 | local funcs = { print=print, doTheThing=utils.doIt } -- create 'global' functions
59 | setmetatable( mydata, {__index=funcs} )
60 | machine:start{ data=mydata }
61 | ```
62 |
63 | ### Callbacks as the Machine Changes
64 |
65 | There are six special keys that you may set to a function value on the machine to keep track of what the machine is doing: `onBeforeExit`, `onAfterEnter`, `onEnteredAll`, `onDataSet`, `onTransition`, and `onEventFired`.
66 |
67 | #### State Change Callbacks
68 |
69 | ```lua
70 | machine.onBeforeExit = function(stateId,stateKind,isAtomic) ... end
71 | machine.onAfterEnter = function(stateId,stateKind,isAtomic) ... end
72 | machine.onEnteredAll = function() ... end
73 | ```
74 |
75 | The state-specific change callbacks are passed three parameters:
76 |
77 | * The string id of the state being exited or entered.
78 | * The string kind of the state: `"state"`, `"parallel"`, or `"final"`.
79 | * _The callbacks are not invoked for `history` or `initial` pseudo-states._
80 | * A boolean indicating whether the state is atomic or not.
81 |
82 | As implied by the names the `onBeforeExit` callback is invoked right **before** leaving a state, while the `onAfterEnter` callback is invoked right **after** entering a state.
83 |
84 | The `onEnteredAll` callback will be invoked once after the last state is entered for a particular _microstep_.
85 |
86 | #### Data Model Callback
87 |
88 | ```lua
89 | machine.onDataSet = function(dataid,newvalue) ... end
90 | ```
91 |
92 | If supplied, this callback will be invoked any time the data model is changed.
93 |
94 | **Warning**: using this callback may slow down the interpreter appreciably, as many internal modifications take place during normal operation (most notably setting the [`_event` system variable](http://www.w3.org/TR/scxml/#SystemVariables)).
95 |
96 | #### Transition Callback
97 |
98 | ```lua
99 | machine.onTransition = function(transitionTable) ... end
100 | ```
101 |
102 | The `onTransition` callback is invoked right before the executable content of a transition (if any) is run.
103 |
104 | **Warning**: the table supplied by this callback is an internal representation whose implementation is not guaranteed to remain unchanged. Currently you can access the following keys for information about the transition:
105 |
106 | * `type` - the string `"internal"` or `"external"`.
107 | * `cond` - the string value of the `cond="…"` attribute, if any, or `nil`.
108 | * `_event` - the string value of the `event="…"` attribute, if any, or `nil`.
109 | * `_target` - the string of the `target="…"` attribute, if any, or `nil`.
110 | * `events` - an array of internal `LXSC.Event` tables, one for each event, or `nil`.
111 | * `targets` - an array of internal `LXSC.State` tables, one for each target, or `nil`.
112 | * Any custom attributes supplied on the transition appear as direct attributes (with no namespace information or protection).
113 |
114 | #### Event Fire Callback
115 |
116 | ```lua
117 | machine.onEventFired = function(eventTable) ... end
118 | ```
119 |
120 | The `onEventFired` callback is invoked whenever `fireEvent()` is called on the machine (either by your own code or by internal machine code). The event has not been processed, and there is no guarantee that the event is going to cause any effect later on. This is mostly a debugging callback allowing you to ensure that events you thought that you were injecting were, in fact, making it in.
121 |
122 | The table supplied to this callback is a `LXSC.Event` object with the following keys:
123 |
124 | * `name` - the string name of the fired event, e.g. `"foo"` or `"foo.bar.jim.jam"`.
125 | * `data` - whatever data (if any) was supplied as the second parameter to `fireEvent()`.
126 | * `triggersDescriptor` - a function that can be used to determine if this event would trigger a particular transition's `event="…"` descriptor.
127 |
128 | ```lua
129 | machine.onEventFired = function(evt)
130 | print(evt.name, evt:triggersDescriptor('a'), evt:triggersDescriptor('a.b'))
131 | end
132 | machine:fireEvent("a") --> a true nil
133 | machine:fireEvent("a.b") --> a true true
134 | ```
135 | * `triggersTransition` - similar to `triggersDescriptor()`, but it takes a transition table (as supplied to the `onTransition` callback) and uses the event descriptor(s) for that transition's `event="..."` attribute to evaluate if the event should cause the transition to be triggered.
136 | * _Note: this does not test any conditional code that may be present inthe transition's `cond="..."` attribute. This function may return true, and then the transition may subsequently not be triggered by this event if the conditions are not right._
137 | * `_tokens` - an array of the event name split by periods (an implementation detail used for optimized transition descriptor matching).
138 |
139 | Note that the event object described above is also returned from `machine:fireEvent()`, in case you need that.
140 |
141 | ### Peeking and Poking at the Data Model
142 |
143 | While the machine is running (after you have called `start()`) you can peek at the data for a specific location via:
144 |
145 | ```lua
146 | local theValue = machine:get("dataId")
147 | ```
148 |
149 | …and you can set the value for a particular location via:
150 |
151 | ```lua
152 | machine:set("dataId",someValue)
153 | ```
154 |
155 | You can evaluate code in the data model (just like a `cond="…"` or `expr="…"` attribute does) by:
156 |
157 | ```lua
158 | local theResult = machine:eval("mycodestring")
159 | ```
160 |
161 | …and you can run arbitrary code against the data model (just like a `
230 |