smalltalk: SUnit port (TestCase/TestSuite/TestResult/TestFailure) + 19 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
@@ -482,6 +482,25 @@
|
||||
;; (no asString here — Symbol/String have their own primitive
|
||||
;; impls; SequenceableCollection-level fallback would overwrite
|
||||
;; the bare-name-for-Symbol behaviour.)
|
||||
;; Array class-side constructors for small fixed-arity literals.
|
||||
(st-class-add-class-method! "Array" "with:"
|
||||
(st-parse-method
|
||||
"with: x | a | a := Array new: 1. a at: 1 put: x. ^ a"))
|
||||
(st-class-add-class-method! "Array" "with:with:"
|
||||
(st-parse-method
|
||||
"with: a with: b
|
||||
| r | r := Array new: 2.
|
||||
r at: 1 put: a. r at: 2 put: b. ^ r"))
|
||||
(st-class-add-class-method! "Array" "with:with:with:"
|
||||
(st-parse-method
|
||||
"with: a with: b with: c
|
||||
| r | r := Array new: 3.
|
||||
r at: 1 put: a. r at: 2 put: b. r at: 3 put: c. ^ r"))
|
||||
(st-class-add-class-method! "Array" "with:with:with:with:"
|
||||
(st-parse-method
|
||||
"with: a with: b with: c with: d
|
||||
| r | r := Array new: 4.
|
||||
r at: 1 put: a. r at: 2 put: b. r at: 3 put: c. r at: 4 put: d. ^ r"))
|
||||
;; ── HashedCollection / Set / Dictionary ──
|
||||
;; Implemented as user instances with array-backed storage. Sets
|
||||
;; use a single `array` ivar; Dictionaries use parallel `keys`/
|
||||
|
||||
153
lib/smalltalk/sunit.sx
Normal file
153
lib/smalltalk/sunit.sx
Normal file
@@ -0,0 +1,153 @@
|
||||
;; SUnit — minimal port written in SX-Smalltalk, run by smalltalk-load.
|
||||
;;
|
||||
;; Provides:
|
||||
;; TestCase — base class. Subclass it, add `testSomething` methods.
|
||||
;; TestSuite — a collection of TestCase instances; runs them all.
|
||||
;; TestResult — passes / failures / errors counts and lists.
|
||||
;; TestFailure — Error subclass raised by `assert:` and friends.
|
||||
;;
|
||||
;; Conventions:
|
||||
;; - Test methods are run in a fresh instance per test.
|
||||
;; - `setUp` is sent before each test; `tearDown` after.
|
||||
;; - Failures are signalled by TestFailure; runner catches and records.
|
||||
|
||||
(define
|
||||
st-sunit-source
|
||||
"Error subclass: #TestFailure
|
||||
instanceVariableNames: ''!
|
||||
|
||||
Object subclass: #TestCase
|
||||
instanceVariableNames: 'testSelector'!
|
||||
|
||||
!TestCase methodsFor: 'access'!
|
||||
testSelector ^ testSelector!
|
||||
testSelector: aSym testSelector := aSym. ^ self! !
|
||||
|
||||
!TestCase methodsFor: 'fixture'!
|
||||
setUp ^ self!
|
||||
tearDown ^ self! !
|
||||
|
||||
!TestCase methodsFor: 'asserts'!
|
||||
assert: aBoolean
|
||||
aBoolean ifFalse: [TestFailure signal: 'assertion failed'].
|
||||
^ self!
|
||||
|
||||
assert: aBoolean description: aString
|
||||
aBoolean ifFalse: [TestFailure signal: aString].
|
||||
^ self!
|
||||
|
||||
assert: actual equals: expected
|
||||
actual = expected ifFalse: [
|
||||
TestFailure signal: 'expected ' , expected printString
|
||||
, ' but got ' , actual printString].
|
||||
^ self!
|
||||
|
||||
deny: aBoolean
|
||||
aBoolean ifTrue: [TestFailure signal: 'denial failed'].
|
||||
^ self!
|
||||
|
||||
should: aBlock raise: anExceptionClass
|
||||
| raised |
|
||||
raised := false.
|
||||
[aBlock value] on: anExceptionClass do: [:e | raised := true].
|
||||
raised ifFalse: [
|
||||
TestFailure signal: 'expected exception ' , anExceptionClass name
|
||||
, ' was not raised'].
|
||||
^ self!
|
||||
|
||||
shouldnt: aBlock raise: anExceptionClass
|
||||
| raised |
|
||||
raised := false.
|
||||
[aBlock value] on: anExceptionClass do: [:e | raised := true].
|
||||
raised ifTrue: [
|
||||
TestFailure signal: 'unexpected exception ' , anExceptionClass name].
|
||||
^ self! !
|
||||
|
||||
!TestCase methodsFor: 'running'!
|
||||
runCase
|
||||
self setUp.
|
||||
self perform: testSelector.
|
||||
self tearDown.
|
||||
^ self! !
|
||||
|
||||
!TestCase class methodsFor: 'instantiation'!
|
||||
selector: aSym ^ self new testSelector: aSym!
|
||||
|
||||
suiteForAll: aSelectorArray
|
||||
| suite |
|
||||
suite := TestSuite new init.
|
||||
suite name: self name.
|
||||
aSelectorArray do: [:s | suite addTest: (self selector: s)].
|
||||
^ suite! !
|
||||
|
||||
Object subclass: #TestResult
|
||||
instanceVariableNames: 'passes failures errors'!
|
||||
|
||||
!TestResult methodsFor: 'init'!
|
||||
init
|
||||
passes := Array new: 0.
|
||||
failures := Array new: 0.
|
||||
errors := Array new: 0.
|
||||
^ self! !
|
||||
|
||||
!TestResult methodsFor: 'access'!
|
||||
passes ^ passes!
|
||||
failures ^ failures!
|
||||
errors ^ errors!
|
||||
passCount ^ passes size!
|
||||
failureCount ^ failures size!
|
||||
errorCount ^ errors size!
|
||||
totalCount ^ passes size + failures size + errors size!
|
||||
|
||||
addPass: aTest passes add: aTest. ^ self!
|
||||
addFailure: aTest message: aMsg
|
||||
| rec |
|
||||
rec := Array new: 2.
|
||||
rec at: 1 put: aTest. rec at: 2 put: aMsg.
|
||||
failures add: rec.
|
||||
^ self!
|
||||
addError: aTest message: aMsg
|
||||
| rec |
|
||||
rec := Array new: 2.
|
||||
rec at: 1 put: aTest. rec at: 2 put: aMsg.
|
||||
errors add: rec.
|
||||
^ self!
|
||||
|
||||
isEmpty ^ self totalCount = 0!
|
||||
allPassed ^ (failures size + errors size) = 0!
|
||||
|
||||
summary
|
||||
^ 'Tests: {1} Passed: {2} Failed: {3} Errors: {4}'
|
||||
format: (Array
|
||||
with: self totalCount printString
|
||||
with: passes size printString
|
||||
with: failures size printString
|
||||
with: errors size printString)! !
|
||||
|
||||
Object subclass: #TestSuite
|
||||
instanceVariableNames: 'tests name'!
|
||||
|
||||
!TestSuite methodsFor: 'init'!
|
||||
init tests := Array new: 0. name := 'Suite'. ^ self!
|
||||
name ^ name!
|
||||
name: aString name := aString. ^ self! !
|
||||
|
||||
!TestSuite methodsFor: 'tests'!
|
||||
tests ^ tests!
|
||||
addTest: aTest tests add: aTest. ^ self!
|
||||
addAll: aCollection aCollection do: [:t | self addTest: t]. ^ self!
|
||||
size ^ tests size! !
|
||||
|
||||
!TestSuite methodsFor: 'running'!
|
||||
run
|
||||
| result |
|
||||
result := TestResult new init.
|
||||
tests do: [:t | self runTest: t result: result].
|
||||
^ result!
|
||||
|
||||
runTest: aTest result: aResult
|
||||
[aTest runCase. aResult addPass: aTest]
|
||||
on: TestFailure do: [:e | aResult addFailure: aTest message: e messageText].
|
||||
^ self! !")
|
||||
|
||||
(smalltalk-load st-sunit-source)
|
||||
@@ -63,10 +63,12 @@ EPOCHS
|
||||
(epoch 4)
|
||||
(load "lib/smalltalk/eval.sx")
|
||||
(epoch 5)
|
||||
(load "lib/smalltalk/tests/tokenize.sx")
|
||||
(load "lib/smalltalk/sunit.sx")
|
||||
(epoch 6)
|
||||
(load "$FILE")
|
||||
(load "lib/smalltalk/tests/tokenize.sx")
|
||||
(epoch 7)
|
||||
(load "$FILE")
|
||||
(epoch 8)
|
||||
(eval "(list st-test-pass st-test-fail)")
|
||||
EPOCHS
|
||||
fi
|
||||
@@ -116,10 +118,12 @@ EPOCHS
|
||||
(epoch 4)
|
||||
(load "lib/smalltalk/eval.sx")
|
||||
(epoch 5)
|
||||
(load "lib/smalltalk/tests/tokenize.sx")
|
||||
(load "lib/smalltalk/sunit.sx")
|
||||
(epoch 6)
|
||||
(load "$FILE")
|
||||
(load "lib/smalltalk/tests/tokenize.sx")
|
||||
(epoch 7)
|
||||
(load "$FILE")
|
||||
(epoch 8)
|
||||
(eval "(map (fn (f) (get f :name)) st-test-fails)")
|
||||
EPOCHS
|
||||
fi
|
||||
|
||||
198
lib/smalltalk/tests/sunit.sx
Normal file
198
lib/smalltalk/tests/sunit.sx
Normal file
@@ -0,0 +1,198 @@
|
||||
;; SUnit port tests. Loads `lib/smalltalk/sunit.sx` (which itself calls
|
||||
;; smalltalk-load to install TestCase/TestSuite/TestResult/TestFailure)
|
||||
;; and exercises the framework on small Smalltalk-defined cases.
|
||||
|
||||
(set! st-test-pass 0)
|
||||
(set! st-test-fail 0)
|
||||
(set! st-test-fails (list))
|
||||
|
||||
;; test.sh loads lib/smalltalk/sunit.sx for us BEFORE this file runs
|
||||
;; (nested SX loads do not propagate top-level forms reliably, so the
|
||||
;; bootstrap chain is concentrated in test.sh). The SUnit classes are
|
||||
;; already present in the class table at this point.
|
||||
|
||||
(define ev (fn (src) (smalltalk-eval src)))
|
||||
(define evp (fn (src) (smalltalk-eval-program src)))
|
||||
|
||||
;; ── 1. Classes installed ──
|
||||
(st-test "TestCase exists" (st-class-exists? "TestCase") true)
|
||||
(st-test "TestSuite exists" (st-class-exists? "TestSuite") true)
|
||||
(st-test "TestResult exists" (st-class-exists? "TestResult") true)
|
||||
(st-test "TestFailure < Error"
|
||||
(st-class-inherits-from? "TestFailure" "Error") true)
|
||||
|
||||
;; ── 2. A subclass with one passing test runs cleanly ──
|
||||
(smalltalk-load
|
||||
"TestCase subclass: #PassingCase
|
||||
instanceVariableNames: ''!
|
||||
|
||||
!PassingCase methodsFor: 'tests'!
|
||||
testOnePlusOne self assert: 1 + 1 = 2! !")
|
||||
|
||||
(st-test "passing test runs and counts as pass"
|
||||
(evp
|
||||
"| suite r |
|
||||
suite := PassingCase suiteForAll: #(#testOnePlusOne).
|
||||
r := suite run.
|
||||
^ r passCount")
|
||||
1)
|
||||
|
||||
(st-test "passing test has no failures"
|
||||
(evp
|
||||
"| suite r |
|
||||
suite := PassingCase suiteForAll: #(#testOnePlusOne).
|
||||
r := suite run.
|
||||
^ r failureCount")
|
||||
0)
|
||||
|
||||
;; ── 3. A subclass with a failing assert: increments failures ──
|
||||
(smalltalk-load
|
||||
"TestCase subclass: #FailingCase
|
||||
instanceVariableNames: ''!
|
||||
|
||||
!FailingCase methodsFor: 'tests'!
|
||||
testFalse self assert: false!
|
||||
testEquals self assert: 1 + 1 equals: 3! !")
|
||||
|
||||
(st-test "assert: false bumps failureCount"
|
||||
(evp
|
||||
"| suite r |
|
||||
suite := FailingCase suiteForAll: #(#testFalse).
|
||||
r := suite run.
|
||||
^ r failureCount")
|
||||
1)
|
||||
|
||||
(st-test "assert:equals: with mismatch fails"
|
||||
(evp
|
||||
"| suite r |
|
||||
suite := FailingCase suiteForAll: #(#testEquals).
|
||||
r := suite run.
|
||||
^ r failureCount")
|
||||
1)
|
||||
|
||||
(st-test "failure messageText captured"
|
||||
(evp
|
||||
"| suite r rec |
|
||||
suite := FailingCase suiteForAll: #(#testEquals).
|
||||
r := suite run.
|
||||
rec := r failures at: 1.
|
||||
^ rec at: 2")
|
||||
"expected 3 but got 2")
|
||||
|
||||
;; ── 4. Mixed pass/fail counts add up ──
|
||||
(smalltalk-load
|
||||
"TestCase subclass: #MixedCase
|
||||
instanceVariableNames: ''!
|
||||
|
||||
!MixedCase methodsFor: 'tests'!
|
||||
testGood self assert: true!
|
||||
testBad self assert: false!
|
||||
testAlsoGood self assert: 2 > 1! !")
|
||||
|
||||
(st-test "mixed suite — totalCount"
|
||||
(evp
|
||||
"| s r |
|
||||
s := MixedCase suiteForAll: #(#testGood #testBad #testAlsoGood).
|
||||
r := s run.
|
||||
^ r totalCount")
|
||||
3)
|
||||
|
||||
(st-test "mixed suite — passCount"
|
||||
(evp
|
||||
"| s r |
|
||||
s := MixedCase suiteForAll: #(#testGood #testBad #testAlsoGood).
|
||||
r := s run.
|
||||
^ r passCount")
|
||||
2)
|
||||
|
||||
(st-test "mixed suite — failureCount"
|
||||
(evp
|
||||
"| s r |
|
||||
s := MixedCase suiteForAll: #(#testGood #testBad #testAlsoGood).
|
||||
r := s run.
|
||||
^ r failureCount")
|
||||
1)
|
||||
|
||||
(st-test "allPassed false on mix"
|
||||
(evp
|
||||
"| s r |
|
||||
s := MixedCase suiteForAll: #(#testGood #testBad #testAlsoGood).
|
||||
r := s run.
|
||||
^ r allPassed")
|
||||
false)
|
||||
|
||||
(st-test "allPassed true with only passes"
|
||||
(evp
|
||||
"| s r |
|
||||
s := MixedCase suiteForAll: #(#testGood #testAlsoGood).
|
||||
r := s run.
|
||||
^ r allPassed")
|
||||
true)
|
||||
|
||||
;; ── 5. setUp / tearDown ──
|
||||
(smalltalk-load
|
||||
"TestCase subclass: #FixtureCase
|
||||
instanceVariableNames: 'value'!
|
||||
|
||||
!FixtureCase methodsFor: 'fixture'!
|
||||
setUp value := 42. ^ self!
|
||||
tearDown ^ self! !
|
||||
|
||||
!FixtureCase methodsFor: 'tests'!
|
||||
testValueIs42 self assert: value = 42! !")
|
||||
|
||||
(st-test "setUp ran before test"
|
||||
(evp
|
||||
"| s r |
|
||||
s := FixtureCase suiteForAll: #(#testValueIs42).
|
||||
r := s run.
|
||||
^ r passCount")
|
||||
1)
|
||||
|
||||
;; ── 6. should:raise: and shouldnt:raise: ──
|
||||
(smalltalk-load
|
||||
"TestCase subclass: #RaiseCase
|
||||
instanceVariableNames: ''!
|
||||
|
||||
!RaiseCase methodsFor: 'tests'!
|
||||
testShouldRaise
|
||||
self should: [Error signal: 'boom'] raise: Error!
|
||||
|
||||
testShouldRaiseFails
|
||||
self should: [42] raise: Error!
|
||||
|
||||
testShouldntRaise
|
||||
self shouldnt: [42] raise: Error! !")
|
||||
|
||||
(st-test "should:raise: catches matching"
|
||||
(evp
|
||||
"| r |
|
||||
r := (RaiseCase suiteForAll: #(#testShouldRaise)) run.
|
||||
^ r passCount") 1)
|
||||
|
||||
(st-test "should:raise: fails when no exception"
|
||||
(evp
|
||||
"| r |
|
||||
r := (RaiseCase suiteForAll: #(#testShouldRaiseFails)) run.
|
||||
^ r failureCount") 1)
|
||||
|
||||
(st-test "shouldnt:raise: passes when nothing thrown"
|
||||
(evp
|
||||
"| r |
|
||||
r := (RaiseCase suiteForAll: #(#testShouldntRaise)) run.
|
||||
^ r passCount") 1)
|
||||
|
||||
;; ── 7. summary string uses format: ──
|
||||
(st-test "summary contains pass count"
|
||||
(let
|
||||
((s (evp
|
||||
"| s r |
|
||||
s := MixedCase suiteForAll: #(#testGood #testBad).
|
||||
r := s run.
|
||||
^ r summary")))
|
||||
(cond
|
||||
((not (string? s)) false)
|
||||
(else (> (len s) 0))))
|
||||
true)
|
||||
|
||||
(list st-test-pass st-test-fail)
|
||||
Reference in New Issue
Block a user