Skip to content

Commit 965d07c

Browse files
authored
Merge pull request #21 from algorandfoundation/feat-utils
feat: implement stubs for urange, assertMatch and match functions
2 parents f08cd9f + cd7b7a2 commit 965d07c

File tree

7 files changed

+280
-7
lines changed

7 files changed

+280
-7
lines changed

src/impl/match.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { assert, assertMatch, internal, match } from '@algorandfoundation/algorand-typescript'
2+
import { ARC4Encoded } from '@algorandfoundation/algorand-typescript/arc4'
3+
import { DeliberateAny } from '../typescript-helpers'
4+
import { asBytes, asMaybeBigUintCls } from '../util'
5+
import { BytesBackedCls, Uint64BackedCls } from './base'
6+
7+
export const matchImpl: typeof match = (subject, test): boolean => {
8+
const bigIntSubjectValue = getBigIntValue(subject)
9+
if (bigIntSubjectValue !== undefined) {
10+
const bigIntTestValue = getBigIntValue(test)
11+
if (bigIntTestValue !== undefined) {
12+
return bigIntSubjectValue === bigIntTestValue
13+
} else if (Object.hasOwn(test, 'lessThan')) {
14+
return bigIntSubjectValue < getBigIntValue((test as DeliberateAny).lessThan)!
15+
} else if (Object.hasOwn(test, 'greaterThan')) {
16+
return bigIntSubjectValue > getBigIntValue((test as DeliberateAny).greaterThan)!
17+
} else if (Object.hasOwn(test, 'lessThanEq')) {
18+
return bigIntSubjectValue <= getBigIntValue((test as DeliberateAny).lessThanEq)!
19+
} else if (Object.hasOwn(test, 'greaterThanEq')) {
20+
return bigIntSubjectValue >= getBigIntValue((test as DeliberateAny).greaterThanEq)!
21+
} else if (Object.hasOwn(test, 'between')) {
22+
const [start, end] = (test as DeliberateAny).between
23+
return bigIntSubjectValue >= getBigIntValue(start)! && bigIntSubjectValue <= getBigIntValue(end)!
24+
}
25+
} else if (subject instanceof internal.primitives.BytesCls) {
26+
return subject.equals(asBytes(test as unknown as internal.primitives.StubBytesCompat))
27+
} else if (typeof subject === 'string') {
28+
return subject === test
29+
} else if (subject instanceof BytesBackedCls) {
30+
return subject.bytes.equals((test as unknown as BytesBackedCls).bytes)
31+
} else if (subject instanceof Uint64BackedCls) {
32+
return (
33+
getBigIntValue(subject.uint64 as unknown as internal.primitives.Uint64Cls) ===
34+
getBigIntValue((test as unknown as Uint64BackedCls).uint64 as unknown as internal.primitives.Uint64Cls)
35+
)
36+
} else if (subject instanceof ARC4Encoded) {
37+
return subject.bytes.equals((test as unknown as ARC4Encoded).bytes)
38+
} else if (Array.isArray(subject)) {
39+
return (test as []).map((x, i) => matchImpl((subject as DeliberateAny)[i], x as DeliberateAny)).every((x) => x)
40+
} else if (typeof subject === 'object') {
41+
return Object.entries(test!)
42+
.map(([k, v]) => matchImpl((subject as DeliberateAny)[k], v as DeliberateAny))
43+
.every((x) => x)
44+
}
45+
return false
46+
}
47+
48+
export const assertMatchImpl: typeof assertMatch = (subject, test, message): boolean => {
49+
const isMatching = matchImpl(subject, test)
50+
assert(isMatching, message)
51+
return isMatching
52+
}
53+
54+
const getBigIntValue = (x: unknown) => {
55+
return asMaybeBigUintCls(x)?.asBigInt()
56+
}

src/impl/urange.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { internal } from '@algorandfoundation/algorand-typescript'
2+
import { asBigInt, asUint64 } from '../util'
3+
4+
export function* urangeImpl(
5+
a: internal.primitives.StubUint64Compat,
6+
b?: internal.primitives.StubUint64Compat,
7+
c?: internal.primitives.StubUint64Compat,
8+
) {
9+
const start = b ? asBigInt(a) : BigInt(0)
10+
const end = b ? asBigInt(b) : asBigInt(a)
11+
const step = c ? asBigInt(c) : BigInt(1)
12+
let iterationCount = 0
13+
for (let i = start; i < end; i += step) {
14+
iterationCount++
15+
yield asUint64(i)
16+
}
17+
return iterationCount
18+
}

src/runtime-helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export { emitImpl } from './impl/emit'
1212
export * from './impl/encoded-types'
1313
export { decodeArc4Impl, encodeArc4Impl } from './impl/encoded-types'
1414
export { ensureBudgetImpl } from './impl/ensure-budget'
15+
export { assertMatchImpl, matchImpl } from './impl/match'
1516
export { TemplateVarImpl } from './impl/template-var'
17+
export { urangeImpl } from './impl/urange'
1618

1719
export function switchableValue(x: unknown): bigint | string | boolean {
1820
if (typeof x === 'boolean') return x

src/subcontexts/ledger-context.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ export class LedgerContext {
2727
this.appIdContractMap.set(appId, contract)
2828
}
2929

30+
getAccount(address: Account): Account {
31+
if (this.accountDataMap.has(address)) {
32+
return Account(address.bytes)
33+
}
34+
throw internal.errors.internalError('Unknown account, check correct testing context is active')
35+
}
36+
3037
getAsset(assetId: internal.primitives.StubUint64Compat): Asset {
3138
if (this.assetDataMap.has(assetId)) {
3239
return Asset(asUint64(assetId))
@@ -57,16 +64,16 @@ export class LedgerContext {
5764
return undefined
5865
}
5966
const entries = this.applicationDataMap.entries()
60-
let next = entries.next().value
67+
let next = entries.next()
6168
let found = false
62-
while (next && !found) {
63-
found = next[1].application.approvalProgram === approvalProgram
69+
while (!next.done && !found) {
70+
found = next.value[1].application.approvalProgram === approvalProgram
6471
if (!found) {
65-
next = entries.next().value
72+
next = entries.next()
6673
}
6774
}
68-
if (found && next) {
69-
const appId = asUint64(next[0])
75+
if (found && next?.value) {
76+
const appId = asUint64(next.value[0])
7077
if (this.applicationDataMap.has(appId)) {
7178
return Application(appId)
7279
}

src/test-transformer/visitors.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,18 @@ const tryGetStubbedFunctionName = (node: ts.CallExpression, helper: VisitorHelpe
367367
if (sourceFileName && !algotsModulePaths.some((s) => sourceFileName.includes(s))) return undefined
368368
}
369369
const functionName = functionSymbol?.getName() ?? identityExpression.text
370-
const stubbedFunctionNames = ['interpretAsArc4', 'decodeArc4', 'encodeArc4', 'TemplateVar', 'ensureBudget', 'emit', 'compile']
370+
const stubbedFunctionNames = [
371+
'interpretAsArc4',
372+
'decodeArc4',
373+
'encodeArc4',
374+
'TemplateVar',
375+
'ensureBudget',
376+
'emit',
377+
'compile',
378+
'urange',
379+
'match',
380+
'assertMatch',
381+
]
371382
return stubbedFunctionNames.includes(functionName) ? functionName : undefined
372383
}
373384

tests/match.spec.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { assertMatch, biguint, BigUint, Bytes, match, Uint64 } from '@algorandfoundation/algorand-typescript'
2+
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
3+
import { afterEach, describe, expect, test } from 'vitest'
4+
import { MAX_UINT512, MAX_UINT64 } from '../src/constants'
5+
import { StrImpl } from '../src/impl/encoded-types'
6+
describe('match', () => {
7+
const ctx = new TestExecutionContext()
8+
9+
afterEach(async () => {
10+
ctx.reset()
11+
})
12+
13+
const numericTestData = [
14+
{ subject: 1, test: 1, expected: true },
15+
{ subject: 0, test: 1, expected: false },
16+
{ subject: 1, test: 0, expected: false },
17+
{ subject: 1, test: Uint64(1), expected: true },
18+
{ subject: Uint64(1), test: Uint64(1), expected: true },
19+
{ subject: Uint64(1), test: 1, expected: true },
20+
{ subject: 42, test: MAX_UINT64, expected: false },
21+
{ subject: Uint64(MAX_UINT64), test: Uint64(42), expected: false },
22+
{ subject: BigUint(1), test: 1n, expected: true },
23+
{ subject: 1n, test: BigUint(1), expected: true },
24+
{ subject: BigUint(1), test: BigUint(1), expected: true },
25+
{ subject: 42n, test: MAX_UINT512, expected: false },
26+
{ subject: BigUint(MAX_UINT512), test: BigUint(42n), expected: false },
27+
{ subject: { a: BigUint(MAX_UINT512) }, test: { a: { lessThan: MAX_UINT512 } }, expected: false },
28+
{ subject: { a: BigUint(MAX_UINT512) }, test: { a: { lessThanEq: MAX_UINT64 } }, expected: false },
29+
{ subject: { a: MAX_UINT64 }, test: { a: { lessThan: BigUint(MAX_UINT512) } }, expected: true },
30+
{ subject: { a: MAX_UINT512 }, test: { a: { lessThanEq: BigUint(MAX_UINT512) } }, expected: true },
31+
{ subject: { a: BigUint(MAX_UINT512) }, test: { a: { greaterThan: MAX_UINT512 } }, expected: false },
32+
{ subject: { a: BigUint(MAX_UINT64) }, test: { a: { greaterThanEq: MAX_UINT512 } }, expected: false },
33+
{ subject: { a: MAX_UINT512 }, test: { a: { greaterThan: BigUint(MAX_UINT64) } }, expected: true },
34+
{ subject: { a: MAX_UINT512 }, test: { a: { greaterThanEq: BigUint(MAX_UINT512) } }, expected: true },
35+
{
36+
subject: { a: MAX_UINT512 },
37+
test: { a: { between: [BigUint(MAX_UINT64), BigUint(MAX_UINT512)] as [biguint, biguint] } },
38+
expected: true,
39+
},
40+
{
41+
subject: { a: MAX_UINT64 },
42+
test: { a: { between: [BigUint(MAX_UINT64), BigUint(MAX_UINT512)] as [biguint, biguint] } },
43+
expected: true,
44+
},
45+
{ subject: { a: 42 }, test: { a: { between: [BigUint(MAX_UINT64), BigUint(MAX_UINT512)] as [biguint, biguint] } }, expected: false },
46+
]
47+
48+
const account1 = ctx.any.account()
49+
const sameAccount = ctx.ledger.getAccount(account1)
50+
const differentAccount = ctx.any.account()
51+
52+
const app1 = ctx.any.application()
53+
const sameApp = ctx.ledger.getApplication(app1.id)
54+
const differentApp = ctx.any.application()
55+
56+
const asset1 = ctx.any.asset()
57+
const sameAsset = ctx.ledger.getAsset(asset1.id)
58+
const differentAsset = ctx.any.application()
59+
60+
const arc4Str1 = ctx.any.arc4.str(10)
61+
const sameArc4Str = new StrImpl((arc4Str1 as StrImpl).typeInfo, arc4Str1.native)
62+
const differentArc4Str = ctx.any.arc4.str(10)
63+
64+
const testData = [
65+
{ subject: '', test: '', expected: true },
66+
{ subject: 'hello', test: 'hello', expected: true },
67+
{ subject: 'hello', test: 'world', expected: false },
68+
{ subject: '', test: 'world', expected: false },
69+
{ subject: Bytes(), test: Bytes(), expected: true },
70+
{ subject: Bytes('hello'), test: Bytes('hello'), expected: true },
71+
{ subject: Bytes('hello'), test: Bytes('world'), expected: false },
72+
{ subject: Bytes(''), test: Bytes('world'), expected: false },
73+
{ subject: account1, test: account1, expected: true },
74+
{ subject: account1, test: sameAccount, expected: true },
75+
{ subject: account1, test: differentAccount, expected: false },
76+
{ subject: app1, test: app1, expected: true },
77+
{ subject: app1, test: sameApp, expected: true },
78+
{ subject: app1, test: differentApp, expected: false },
79+
{ subject: asset1, test: asset1, expected: true },
80+
{ subject: asset1, test: sameAsset, expected: true },
81+
{ subject: asset1, test: differentAsset, expected: false },
82+
{ subject: arc4Str1, test: arc4Str1, expected: true },
83+
{ subject: arc4Str1, test: sameArc4Str, expected: true },
84+
{ subject: arc4Str1, test: differentArc4Str, expected: false },
85+
{ subject: { a: 'hello', b: 42, c: arc4Str1 }, test: { a: 'hello', b: { lessThanEq: 42 }, c: sameArc4Str }, expected: true },
86+
{ subject: { a: 'hello', b: 42, c: arc4Str1 }, test: { c: sameArc4Str }, expected: true },
87+
{ subject: { a: 'hello', b: 42, c: arc4Str1 }, test: { c: differentArc4Str }, expected: false },
88+
{ subject: ['hello', 42, arc4Str1], test: ['hello', { lessThanEq: 42 }, sameArc4Str], expected: true },
89+
{ subject: ['hello', 42, arc4Str1], test: ['hello'], expected: true },
90+
{ subject: ['hello', 42, arc4Str1], test: ['world'], expected: false },
91+
]
92+
93+
test.each(numericTestData)('should be able to match numeric data %s', (data) => {
94+
const { subject, test, expected } = data
95+
expect(match(subject, test)).toBe(expected)
96+
})
97+
98+
test.each(testData)('should be able to match %s', (data) => {
99+
const { subject, test, expected } = data
100+
expect(match(subject, test)).toBe(expected)
101+
})
102+
103+
test.each(numericTestData.filter((x) => x.expected))('should be able to assert match numeric data %s', (data) => {
104+
const { subject, test, expected } = data
105+
expect(assertMatch(subject, test)).toBe(expected)
106+
})
107+
108+
test.each(testData.filter((x) => x.expected))('should be able to assert match %s', (data) => {
109+
const { subject, test, expected } = data
110+
expect(match(subject, test)).toBe(expected)
111+
})
112+
113+
test.each(numericTestData.filter((x) => !x.expected))('should throw exception when assert match fails for numeric data %s', (data) => {
114+
const { subject, test } = data
115+
expect(() => assertMatch(subject, test)).toThrow('Assertion failed')
116+
})
117+
118+
test.each(testData.filter((x) => !x.expected))('should throw exception when assert match fails %s', (data) => {
119+
const { subject, test } = data
120+
expect(() => assertMatch(subject, test)).toThrow('Assertion failed')
121+
})
122+
})

tests/urange.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Uint64, urange } from '@algorandfoundation/algorand-typescript'
2+
import { describe, expect, it } from 'vitest'
3+
4+
describe('urange', () => {
5+
it('should iterate from 0 to a-1 when only a is provided', () => {
6+
const a = Uint64(5)
7+
const iterator = urange(a)
8+
const result = []
9+
10+
for (let item = iterator.next(); !item.done; item = iterator.next()) {
11+
result.push(item.value)
12+
}
13+
14+
expect(result).toEqual([BigInt(0), BigInt(1), BigInt(2), BigInt(3), BigInt(4)])
15+
})
16+
17+
it('should iterate from a to b-1 when a and b are provided', () => {
18+
const a = Uint64(2)
19+
const b = Uint64(5)
20+
const iterator = urange(a, b)
21+
const result = []
22+
23+
for (let item = iterator.next(); !item.done; item = iterator.next()) {
24+
result.push(item.value)
25+
}
26+
27+
expect(result).toEqual([BigInt(2), BigInt(3), BigInt(4)])
28+
})
29+
30+
it('should iterate from a to b-1 with step c when a, b, and c are provided', () => {
31+
const a = Uint64(2)
32+
const b = Uint64(10)
33+
const c = Uint64(2)
34+
const iterator = urange(a, b, c)
35+
const result = []
36+
37+
for (let item = iterator.next(); !item.done; item = iterator.next()) {
38+
result.push(item.value)
39+
}
40+
41+
expect(result).toEqual([BigInt(2), BigInt(4), BigInt(6), BigInt(8)])
42+
})
43+
44+
it('should return iteration count when done', () => {
45+
const a = Uint64(3)
46+
const iterator = urange(a)
47+
let item = iterator.next()
48+
let count = 0
49+
50+
while (!item.done) {
51+
count++
52+
item = iterator.next()
53+
}
54+
55+
expect(item.value).toBe(count)
56+
})
57+
})

0 commit comments

Comments
 (0)