# Tests for the cron module
#
# This file contains a collection of tests for one or more of the Tcl
# built-in commands. Sourcing this file into Tcl runs the tests and
# generates output for errors. No output means no errors were found.
#
# Copyright (c) 2016 by Sean Woods
# (Insert BSDish style "use at your own risk" license text)
source [file join \
[file dirname [file dirname [file join [pwd] [info script]]]] \
devtools testutilities.tcl]
package require tcltest
testsNeedTcl 8.6
testsNeedTcltest 1.0
support {
use dicttool/dicttool.tcl dicttool
}
testing {
useLocal cron.tcl cron
}
###
# For the first part of our testing, control the clock
# via the test harness
###
set ::cron::trace 0
set ::cron::time [expr {[clock scan {2016-01-01}]*1000}]
foreach {val testval} {
1000 1000
11235 11000
1241241 1241000
} {
test cron-step-$val [list test clock_step function for $val] {
::cron::clock_step $val
} $testval
}
proc test_elapsed_time {start target} {
set now [::cron::current_time]
set value [expr {$now-$start}]
if {$value < ($target-5)} {
puts "ELAPSED TIME WAS SHORT: $value / $target"
return 1
}
if {$value > ($target+250)} {
puts "ELAPSED TIME WAS LONG: $value / $target"
return 1
}
return 0
}
set start [::cron::current_time]
::cron::sleep 250
test cron-sleep-1 {Ensure sleep is in a plausible range} {
test_elapsed_time $start 250
} 0
# Sleep until the top of the second
::cron::clock_sleep 1
set start [::cron::current_time]
::cron::clock_sleep 0 750
test cron-sleep-2 {Ensure sleep is in a plausible range} {
test_elapsed_time $start 750
} 0
::cron::clock_sleep 1 0
test cron-sleep-3 {Ensure sleep is in a plausible range} {
test_elapsed_time $start 1000
} 0
::cron::clock_sleep 1 0
###
# Object interaction tests
###
oo::class create CronTest {
method coro_name {} {
return [info object namespace [self]]::idle
}
method idle {} {
set coro [my coro_name]
::cron::object_coroutine [self] $coro
::coroutine $coro {*}[namespace code {my IdleTask}]
}
}
###
# This test is a mockup of typical Tk widget
# which has some portion of its startup that has to
# process after an idle loop has completed
###
oo::class create CronTest_3Pings {
superclass CronTest
constructor {} {
set ::TESTOBJ([self]) 0
my idle
}
method IdleTask {} {
incr ::TESTOBJ([self])
yield
incr ::TESTOBJ([self])
yield
incr ::TESTOBJ([self])
}
}
CronTest_3Pings create FOO
set coro [FOO coro_name]
###
# The coroutine for the object exist on startup
test cron-objects-1-1 {cron::every} {
info commands $coro
} $coro
# And CRON knows about it
test cron-objects-1-2 {cron::every} {
::cron::task exists $coro
} 1
# The counter should be initialized to the value
# before the first yield
test cron-objects-1-3 {cron::every} {
set ::TESTOBJ(::FOO)
} 1
::cron::clock_sleep 1
###
# The coroutine should have completed, and now ceases to exist
###
test cron-objects-1-4 {cron::every} {
::cron::task exists $coro
} 0
# The counter should be 3
test cron-objects-1-5 {cron::every} {
set ::TESTOBJ(::FOO)
} 3
###
# Test that cron cleans up after a destroyed object
###
CronTest_3Pings create FOOBAR
set coro [FOOBAR coro_name]
###
# The coroutine for the object exist on startup
test cron-objects-2-1 {cron::every} {
info commands $coro
} $coro
# However CRON knows about it
test cron-objects-2-2 {cron::every} {
::cron::task exists $coro
} 1
FOOBAR destroy
# The idle routine did parse up to the first yield
test cron-objects-2-3 {cron::every} {
set ::TESTOBJ(::FOOBAR)
} 1
###
# The coroutine for the object exist on startup
test cron-objects-2-4 {cron::every} {
info commands $coro
} {}
# However CRON knows about it
test cron-objects-2-5 {cron::every} {
::cron::task exists $coro
} 1
# Trigger the idle loop
::cron::do_one_event TEST
# The idle routine did parse up to the first yield
test cron-objects-2-6 {cron::every} {
set ::TESTOBJ(::FOOBAR)
} 1
# The coroutine is still gone
test cron-objects-2-7 {cron::every} {
info commands $coro
} {}
# And now cron has forgotten about the object
test cron-objects-2-8 {cron::every} {
::cron::task exists $coro
} 0
::cron::do_one_event TEST
test cron-objects-2-9 {cron::every} {
info commands $coro
} {}
# However cron has forgotten about the object
test cron-objects-2-10 {cron::every} {
::cron::task exists $coro
} 0
oo::class create CronTest_Persistant_Coro {
superclass CronTest
constructor {} {
set nspace [info object namespace [self]]
set coro_do [my coro_name DoLoop]
set ::TESTOBJ([self]) -1
set now [::cron::current_time]
set frequency 1000
set scheduled [::cron::clock_step [expr {$now+$frequency}]]
::cron::object_coroutine [self] $coro_do [list frequency $frequency scheduled $scheduled command [namespace code {my DoLoop}]]
}
method coro_name {which} {
return [info object namespace [self]]::${which}
}
method exit_loop {} {
my variable doloop
set doloop 0
if {$::cron::trace} {
puts [list [self] SIGNAL TO EXIT]
}
}
method DoLoop {} {
if {$::cron::trace} {
puts "[self] CORO START"
}
my variable doloop
set doloop 1
set ::TESTOBJ([self]) 0
yield
while {$doloop} {
if {$::cron::trace} {
puts [list [self] LOOP $doloop]
}
incr ::TESTOBJ([self])
yield
}
if {$::cron::trace} {
puts "[self] CORO EXIT"
}
}
}
###
# This series of tests is built around a more complex case:
# an object wants a method invoked periodically. CRON
# will create a coroutine (based on the name given by the object)
# and invoke that coroutine at the frequency requested
#
# If the coroutine exits (or throws an error) It will be restarted
###
set ::cron::trace 0
::cron::clock_sleep 1
CronTest_Persistant_Coro create IRONBAR
set coro [IRONBAR coro_name DoLoop]
test cron-objects-3-1 {
The actual coroutine should not exist yet
} {
info commands $coro
} {}
# And CRON knows about it
test cron-objects-3-2 {
CRON should be aware of the task
} {
::cron::task exists $coro
} 1
test cron-objects-3-3 {
The counter should be initialized to the value
before the first yield
} {
set ::TESTOBJ(::IRONBAR)
} -1
set start [::cron::current_time]
::cron::clock_sleep 1
test cron-objects-3-4 {The coroutine for the object exists} {
info commands $coro
} $coro
test cron-objects-3-5 {Cron should know about the task} {
::cron::task exists $coro
} 1
test cron-objects-3-6 {The counter should have incremented} {
set ::TESTOBJ(::IRONBAR)
} 1
::cron::clock_sleep 0 500
test cron-objects-3-7 {The counter should have incremented} {
set ::TESTOBJ(::IRONBAR)
} 1
::cron::clock_sleep 1
# Test a graceful exit of the coroutine
::IRONBAR exit_loop
::cron::clock_sleep 1
set coro [IRONBAR coro_name DoLoop]
test cron-objects-3-8 {
The actual coroutine should now exit
} {
info commands $coro
} {}
test cron-objects-3-9 {
CRON should still be aware of the tast
} {
::cron::task exists $coro
} 1
test cron-objects-3-10 {The counter hasn't reset} {
set ::TESTOBJ(::IRONBAR)
} 2
::cron::clock_sleep 1
test cron-objects-3-11 {The should have reset when the coroutine restarted} {
set ::TESTOBJ(::IRONBAR)
} 1
#::cron::object_destroy ::IRONBAR
::IRONBAR destroy
set ::cron::trace 0
proc my_coro {} {
if {$::cron::trace} {
puts "START MY CORO"
}
set ::my_coro_progress 0
set ::my_coro_start [::cron::current_time]
if {$::cron::trace} {
puts "SLEEP MY CORO"
}
::cron::sleep 1250
if {$::cron::trace} {
puts "/SLEEP MY CORO"
}
set ::my_coro_end [::cron::current_time]
set ::my_coro_progress 1
if {$::cron::trace} {
puts "END MY CORO"
}
}
###
# Test that an otherwise inprepared coroutine
# which invokes "::cron::sleep" partipates in
# the ::cron event system
###
if {$::cron::trace} {
puts "PRE-MY CORO"
}
coroutine ::TESTCORO my_coro
if {$::cron::trace} {
puts "POST-MY CORO"
}
test cron-naive-corotine-1 {cron::coroutine sleep} {
set ::my_coro_progress
} 0
::cron::clock_sleep 3
set ::cron::trace 0
test cron-naive-corotine-2 {cron::coroutine sleep} {
set ::my_coro_progress
} 1
test cron-naive-corotine-3 {cron::coroutine sleep} {
set delay [expr {($::my_coro_end - $::my_coro_start)}]
if {$delay < 1000 || $delay > 2000} {
puts "TIME DELAY OUT OF RANGE: $delay"
return 1
} else {
return 0
}
} 0
###
# Tests after this point test interactions with the Tcl event loop
# We need to be slaved to the real time clock to work properly
###
set ::cron::trace 0
set ::cron::time -1
###
# Test the clock sleep offset feature
###
# Reset to the top of a clock step
::cron::clock_sleep 1
set ::cron::trace 0
set start [::cron::current_time]
set ::FLAG -1
set time_0 [::cron::clock_delay 1000]
set time_1 [::cron::clock_delay 2000]
after $time_0 {set ::FLAG 0}
after $time_1 {set ::FLAG 1}
test cron-delay-1 {Prior to the first event the value should not have changed} {
set ::FLAG
} -1
vwait ::FLAG
test cron-delay-3 {At the top of the second, we should have a new value for flag} {
set ::FLAG
} 0
vwait ::FLAG
test cron-delay-5 {At the top of the second second, we should have a new value for flag} {
set ::FLAG
} 1
set ::cron::trace 0
proc elapsed_time_coro {} {
set ::start [::cron::current_time]
while 1 {
set now [::cron::current_time]
set ::elapsed_time [expr {($now-$::start)/1000}]
yield
}
}
::cron::task set ::ELAPSED_TIME \
coroutine ::ELAPSED_TIME \
command elapsed_time_coro \
frequency 1000
set timecounter 0
::cron::every timecounter 1 {incr timecounter}
set now [clock seconds]
# Test at
set timerevent 0
::cron::at timeevent1 [expr {$now + 5}] {set ::timerevent 1}
::cron::at timeevent2 [expr {$now + 6}] {set ::eventpause 0}
::cron::at timeevent3 [expr {$now + 10}] {set ::timerevent 2}
::cron::at timeevent4 [expr {$now + 11}] {set ::pause 0}
test cron-1.1 {cron::every} {
set ::timecounter
} 0
test cron-1.2 {cron::at1} {
set ::timerevent
} 0
vwait eventpause
test cron-1.3 {cron::at1} {
set ::timerevent
} 1
###
# At this point 6 seconds should have passed
###
#test cron-1.elapsed-1 {Elapsed time} {
# set ::elapsed_time
#} 5
# - Test removed - Was too unstable on a busy computer
vwait pause
###
# At this point 11 seconds should have passed
###
#test cron-1.elapsed-2 {Elapsed time} {
# set ::elapsed_time
#} 10
# - Test removed - Was too unstable on a busy computer
# Test that in X seconds our timer
# was incremented X times
#test cron-1.4 {cron::every} {
# set ::timecounter
#} $::elapsed_time
#
# - Test removed - Was too unstable on a busy computer
test cron-1.5 {cron::at2} {
set ::timerevent
} 2
###
# Confirm cancel works
::cron::cancel timecounter
set timecounterfinal $::timecounter
::cron::clock_sleep 2
test cron-1.6 {cron::cancel} {
set ::timecounter
} $::timecounterfinal
###
# Test the new IN command
###
set ::inevent 0
cron::in 5 {set ::inevent 1}
test cron-1.7 {cron::in} {
set ::inevent
} 0
::cron::clock_sleep 6
test cron-1.8 {cron::in} {
set ::inevent
} 1
set FAILED 0
after 10000 {set ::cron::forever 0 ; set FAILED 1}
::cron::in 5 {
set ::cron::forever 0
test cron-1.12 {cron::main} {
set ::cron::forever
} 0
}
::cron::wake TEST
###
# At this point 22 seconds should have passed
###
#test cron-1.elapsed-3 {Elapsed time} {
# set ::elapsed_time
#} 21
#
# Test removed - it was too unstable on a real working computer
::cron::main
# If we get to this test, mission successful
test cron-1.13 {cron::main} {
return 1
} 1
test cron-1.14 {cron::main} {
set FAILED
} 0
testsuiteCleanup
return