Skip to content

Commit f8005bf

Browse files
committed
Add AOT-compiled Clojure test suite
1 parent d6aaa83 commit f8005bf

11 files changed

Lines changed: 843 additions & 57 deletions

File tree

Makefile

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ TEST-SUITE-BRANCH := glojure
7878
TEST-SUITE-DIR := test/clojure-test-suite
7979
TEST-SUITE-FILE := test-glojure.glj
8080
TEST-SUITE-EXPECT-FAILURES ?= 0
81-
TEST-SUITE-EXPECT-ERRORS ?= 0
81+
TEST-SUITE-EXPECT-ERRORS ?= 1
82+
TEST-SUITE-EXPECT-LOAD-ERRORS ?= 8
8283

8384
MAKES-CLEAN := \
8485
report.html \
@@ -219,17 +220,31 @@ test-compare: $(YS)
219220
test-aot-runtime: $(GO)
220221
go test -tags glj_aot_runtime ./pkg/glj ./pkg/gljmain ./pkg/runtime
221222

223+
.PHONY: test-aot test-suite-aot
224+
test-aot: test-aot-runtime test-glj
225+
$(MAKE) test-suite-aot
226+
227+
test-suite-aot: $(GO) $(STDLIB-TARGETS) generate aot $(TEST-SUITE-DIR)
228+
cd $(TEST-SUITE-DIR) && git checkout $(TEST-SUITE-BRANCH)
229+
scripts/patch-test-suite $(abspath $(TEST-SUITE-DIR))
230+
TEST_SUITE_EXPECT_FAILURES=$(TEST-SUITE-EXPECT-FAILURES) \
231+
TEST_SUITE_EXPECT_ERRORS=$(TEST-SUITE-EXPECT-ERRORS) \
232+
TEST_SUITE_EXPECT_LOAD_ERRORS=$(TEST-SUITE-EXPECT-LOAD-ERRORS) \
233+
scripts/test-suite-aot $(abspath $(TEST-SUITE-DIR))
234+
222235
test-glj: $(TEST-GLJ-TARGETS)
223236

224237
$(TEST-SUITE-DIR):
225238
git clone --branch $(TEST-SUITE-BRANCH) $(TEST-SUITE-REPO) $@
226239

227240
test-suite: $(GLJ-CMD) $(TEST-SUITE-DIR)
228241
cd $(TEST-SUITE-DIR) && git checkout $(TEST-SUITE-BRANCH)
242+
scripts/patch-test-suite $(abspath $(TEST-SUITE-DIR))
229243
cd $(TEST-SUITE-DIR) && \
230244
$(abspath $<) $(TEST-SUITE-FILE) \
231245
$(if $(TEST-SUITE-EXPECT-FAILURES),--expect-failures $(TEST-SUITE-EXPECT-FAILURES)) \
232246
$(if $(TEST-SUITE-EXPECT-ERRORS),--expect-errors $(TEST-SUITE-EXPECT-ERRORS)) \
247+
--expect-load-errors $(TEST-SUITE-EXPECT-LOAD-ERRORS) \
233248
$(if $(v),,2>/dev/null)
234249

235250
$(TEST-GLJ-TARGETS): $(GLJ-CMD)

pkg/glj/init-javacompat.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
_ "github.com/glojurelang/glojure/pkg/javacompat/instant"
1111
_ "github.com/glojurelang/glojure/pkg/javacompat/integer"
1212
_ "github.com/glojurelang/glojure/pkg/javacompat/long"
13+
_ "github.com/glojurelang/glojure/pkg/javacompat/mapentry"
1314
_ "github.com/glojurelang/glojure/pkg/javacompat/math"
1415
_ "github.com/glojurelang/glojure/pkg/javacompat/regex"
1516
_ "github.com/glojurelang/glojure/pkg/javacompat/string"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Package mapentry exposes the clojure.lang.MapEntry static factory used by
2+
// portable Clojure tests and libraries.
3+
package mapentry
4+
5+
import (
6+
"reflect"
7+
8+
"github.com/glojurelang/glojure/pkg/lang"
9+
"github.com/glojurelang/glojure/pkg/pkgmap"
10+
)
11+
12+
const pkg = "github.com/glojurelang/glojure/pkg/javacompat/mapentry"
13+
14+
// Create mirrors clojure.lang.MapEntry/create.
15+
func Create(key, value any) *lang.MapEntry {
16+
return lang.NewMapEntry(key, value)
17+
}
18+
19+
func init() {
20+
pkgmap.Set(pkg+".Create", Create)
21+
pkgmap.Set("MapEntry.create", Create)
22+
pkgmap.Set("clojure.lang.MapEntry.create", Create)
23+
pkgmap.SetHostClassPackage("MapEntry", "clojure.lang")
24+
pkgmap.SetHostClass(
25+
"MapEntry",
26+
lang.NewClass(reflect.TypeOf((*lang.MapEntry)(nil)), "clojure.lang.MapEntry"),
27+
)
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package mapentry
2+
3+
import (
4+
"testing"
5+
6+
"github.com/glojurelang/glojure/pkg/lang"
7+
"github.com/glojurelang/glojure/pkg/pkgmap"
8+
)
9+
10+
func TestCreateAndRegistration(t *testing.T) {
11+
entry := Create("key", "value")
12+
if entry.Key() != "key" || entry.Val() != "value" {
13+
t.Fatalf("Create returned [%v %v]", entry.Key(), entry.Val())
14+
}
15+
16+
for _, name := range []string{"MapEntry.create", "clojure.lang.MapEntry.create"} {
17+
value, ok := pkgmap.Get(name)
18+
if !ok {
19+
t.Fatalf("%s is not registered", name)
20+
}
21+
create, ok := value.(func(any, any) *lang.MapEntry)
22+
if !ok {
23+
t.Fatalf("%s has type %T", name, value)
24+
}
25+
if got := create("k", "v"); got.Key() != "k" || got.Val() != "v" {
26+
t.Fatalf("registered Create returned [%v %v]", got.Key(), got.Val())
27+
}
28+
}
29+
}

pkg/lang/atom.go

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package lang
22

3-
import "sync/atomic"
3+
import (
4+
"sync"
5+
"sync/atomic"
6+
)
47

58
type (
69
Atom struct {
710
state atomic.Pointer[Box]
811
initial Box
9-
watches IPersistentMap
1012

11-
meta IPersistentMap
13+
referenceMu sync.RWMutex
14+
watches IPersistentMap
15+
validator IFn
16+
meta IPersistentMap
1217
}
1318
)
1419

@@ -36,23 +41,49 @@ func (a *Atom) Deref() interface{} {
3641
return a.state.Load().val
3742
}
3843

39-
func (a *Atom) SetValidator(vf IFn) { panic("not implemented") }
40-
func (a *Atom) Validator() IFn { panic("not implemented") }
44+
func (a *Atom) SetValidator(vf IFn) {
45+
if vf != nil && !IsTruthy(Apply1(vf, a.Deref())) {
46+
panic(NewIllegalStateError("Invalid reference state"))
47+
}
48+
a.referenceMu.Lock()
49+
a.validator = vf
50+
a.referenceMu.Unlock()
51+
}
52+
53+
func (a *Atom) Validator() IFn {
54+
a.referenceMu.RLock()
55+
defer a.referenceMu.RUnlock()
56+
return a.validator
57+
}
58+
59+
// GetValidator is the JVM-style IRef alias used by Clojure host interop.
60+
func (a *Atom) GetValidator() IFn {
61+
return a.Validator()
62+
}
63+
4164
func (a *Atom) Watches() IPersistentMap {
65+
a.referenceMu.RLock()
66+
defer a.referenceMu.RUnlock()
4267
return a.watches
4368
}
4469

4570
func (a *Atom) AddWatch(key interface{}, fn IFn) IRef {
71+
a.referenceMu.Lock()
72+
defer a.referenceMu.Unlock()
4673
a.watches = a.watches.Assoc(key, fn).(IPersistentMap)
4774
return a
4875
}
4976

5077
func (a *Atom) RemoveWatch(key interface{}) {
78+
a.referenceMu.Lock()
79+
defer a.referenceMu.Unlock()
5180
a.watches = a.watches.Without(key)
5281
}
5382

5483
func (a *Atom) notifyWatches(oldVal, newVal interface{}) {
84+
a.referenceMu.RLock()
5585
watches := a.watches
86+
a.referenceMu.RUnlock()
5687
if watches == nil || watches.Count() == 0 {
5788
return
5889
}
@@ -66,10 +97,26 @@ func (a *Atom) notifyWatches(oldVal, newVal interface{}) {
6697
}
6798
}
6899

100+
func (a *Atom) validate(newVal interface{}) {
101+
a.referenceMu.RLock()
102+
validator := a.validator
103+
a.referenceMu.RUnlock()
104+
if validator != nil && !IsTruthy(Apply1(validator, newVal)) {
105+
panic(NewIllegalStateError("Invalid reference state"))
106+
}
107+
}
108+
109+
func (a *Atom) hasWatches() bool {
110+
a.referenceMu.RLock()
111+
defer a.referenceMu.RUnlock()
112+
return a.watches != nil && a.watches.Count() != 0
113+
}
114+
69115
func (a *Atom) Swap(f IFn, args ISeq) interface{} {
70116
for {
71117
old := a.state.Load()
72118
nw := f.ApplyTo(NewCons(old.val, args))
119+
a.validate(nw)
73120
if a.compareAndSetBox(old, nw) {
74121
return nw
75122
}
@@ -83,6 +130,7 @@ func (a *Atom) Swap0(f IFn) interface{} {
83130
for {
84131
old := a.state.Load()
85132
nw := Apply1(f, old.val)
133+
a.validate(nw)
86134
if a.compareAndSetBox(old, nw) {
87135
return nw
88136
}
@@ -93,6 +141,7 @@ func (a *Atom) Swap1(f IFn, x interface{}) interface{} {
93141
for {
94142
old := a.state.Load()
95143
nw := Apply2(f, old.val, x)
144+
a.validate(nw)
96145
if a.compareAndSetBox(old, nw) {
97146
return nw
98147
}
@@ -103,6 +152,7 @@ func (a *Atom) Swap2(f IFn, x, y interface{}) interface{} {
103152
for {
104153
old := a.state.Load()
105154
nw := Apply3(f, old.val, x, y)
155+
a.validate(nw)
106156
if a.compareAndSetBox(old, nw) {
107157
return nw
108158
}
@@ -114,13 +164,13 @@ func (a *Atom) CompareAndSet(oldv, newv interface{}) bool {
114164
if !Identical(old.val, oldv) {
115165
return false
116166
}
167+
a.validate(newv)
117168
return a.compareAndSetBox(old, newv)
118169
}
119170

120171
func (a *Atom) compareAndSetBox(old *Box, newv interface{}) bool {
121-
// TODO: validate
122172
if Identical(old.val, newv) &&
123-
(a.watches == nil || a.watches.Count() == 0) {
173+
!a.hasWatches() {
124174
return a.state.CompareAndSwap(old, old)
125175
}
126176
swapped := a.state.CompareAndSwap(old, NewBox(newv))
@@ -131,11 +181,11 @@ func (a *Atom) compareAndSetBox(old *Box, newv interface{}) bool {
131181
}
132182

133183
func (a *Atom) Reset(newVal interface{}) interface{} {
134-
// TODO: validate
184+
a.validate(newVal)
135185
for {
136186
old := a.state.Load()
137187
if Identical(old.val, newVal) &&
138-
(a.watches == nil || a.watches.Count() == 0) {
188+
!a.hasWatches() {
139189
if a.state.CompareAndSwap(old, old) {
140190
return newVal
141191
}
@@ -148,8 +198,19 @@ func (a *Atom) Reset(newVal interface{}) interface{} {
148198
}
149199

150200
func (a *Atom) Meta() IPersistentMap {
151-
if a.meta == nil {
152-
return nil
153-
}
201+
a.referenceMu.RLock()
202+
defer a.referenceMu.RUnlock()
154203
return a.meta
155204
}
205+
206+
func (a *Atom) AlterMeta(f IFn, args ISeq) IPersistentMap {
207+
meta := ApplySeq(f, NewCons(a.Meta(), args)).(IPersistentMap)
208+
return a.ResetMeta(meta)
209+
}
210+
211+
func (a *Atom) ResetMeta(meta IPersistentMap) IPersistentMap {
212+
a.referenceMu.Lock()
213+
a.meta = meta
214+
a.referenceMu.Unlock()
215+
return meta
216+
}

pkg/lang/atom_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package lang
22

3-
import "testing"
3+
import (
4+
"reflect"
5+
"testing"
6+
)
47

58
func TestAtomCompareAndSetUsesIdentity(t *testing.T) {
69
value := []int{1, 2, 3}
@@ -68,3 +71,47 @@ func TestAtomIdenticalUpdatesStillNotifyWatches(t *testing.T) {
6871
t.Fatalf("watch called %d times, want 2", calls)
6972
}
7073
}
74+
75+
func TestAtomMetadata(t *testing.T) {
76+
atom := NewAtom(nil)
77+
meta := NewMap(NewKeyword("source"), "test")
78+
79+
if got := atom.ResetMeta(meta); got != meta {
80+
t.Fatalf("ResetMeta returned %v, want metadata map", got)
81+
}
82+
if got := atom.Meta(); got != meta {
83+
t.Fatalf("Meta returned %v, want metadata map", got)
84+
}
85+
86+
updated := atom.AlterMeta(FnFunc1(func(current any) any {
87+
return current.(IPersistentMap).Assoc(NewKeyword("updated"), true)
88+
}), nil)
89+
if !Equals(updated.ValAt(NewKeyword("updated")), true) {
90+
t.Fatalf("AlterMeta returned %v without update", updated)
91+
}
92+
}
93+
94+
func TestAtomValidator(t *testing.T) {
95+
atom := NewAtom(int64(2))
96+
even := FnFunc1(func(value any) any {
97+
return value.(int64)%2 == 0
98+
})
99+
atom.SetValidator(even)
100+
101+
if reflect.ValueOf(atom.Validator()).Pointer() != reflect.ValueOf(even).Pointer() {
102+
t.Fatal("Validator did not return the installed function")
103+
}
104+
if got := atom.Reset(int64(4)); got != int64(4) {
105+
t.Fatalf("validated Reset returned %v", got)
106+
}
107+
108+
defer func() {
109+
if recover() == nil {
110+
t.Fatal("validator accepted invalid state")
111+
}
112+
if got := atom.Deref(); got != int64(4) {
113+
t.Fatalf("failed validation changed atom to %v", got)
114+
}
115+
}()
116+
atom.Reset(int64(3))
117+
}

0 commit comments

Comments
 (0)