diff --git a/src/impl/match.ts b/src/impl/match.ts new file mode 100644 index 0000000..da87e3f --- /dev/null +++ b/src/impl/match.ts @@ -0,0 +1,56 @@ +import { assert, assertMatch, internal, match } from '@algorandfoundation/algorand-typescript' +import { ARC4Encoded } from '@algorandfoundation/algorand-typescript/arc4' +import { DeliberateAny } from '../typescript-helpers' +import { asBytes, asMaybeBigUintCls } from '../util' +import { BytesBackedCls, Uint64BackedCls } from './base' + +export const matchImpl: typeof match = (subject, test): boolean => { + const bigIntSubjectValue = getBigIntValue(subject) + if (bigIntSubjectValue !== undefined) { + const bigIntTestValue = getBigIntValue(test) + if (bigIntTestValue !== undefined) { + return bigIntSubjectValue === bigIntTestValue + } else if (Object.hasOwn(test, 'lessThan')) { + return bigIntSubjectValue < getBigIntValue((test as DeliberateAny).lessThan)! + } else if (Object.hasOwn(test, 'greaterThan')) { + return bigIntSubjectValue > getBigIntValue((test as DeliberateAny).greaterThan)! + } else if (Object.hasOwn(test, 'lessThanEq')) { + return bigIntSubjectValue <= getBigIntValue((test as DeliberateAny).lessThanEq)! + } else if (Object.hasOwn(test, 'greaterThanEq')) { + return bigIntSubjectValue >= getBigIntValue((test as DeliberateAny).greaterThanEq)! + } else if (Object.hasOwn(test, 'between')) { + const [start, end] = (test as DeliberateAny).between + return bigIntSubjectValue >= getBigIntValue(start)! && bigIntSubjectValue <= getBigIntValue(end)! + } + } else if (subject instanceof internal.primitives.BytesCls) { + return subject.equals(asBytes(test as unknown as internal.primitives.StubBytesCompat)) + } else if (typeof subject === 'string') { + return subject === test + } else if (subject instanceof BytesBackedCls) { + return subject.bytes.equals((test as unknown as BytesBackedCls).bytes) + } else if (subject instanceof Uint64BackedCls) { + return ( + getBigIntValue(subject.uint64 as unknown as internal.primitives.Uint64Cls) === + getBigIntValue((test as unknown as Uint64BackedCls).uint64 as unknown as internal.primitives.Uint64Cls) + ) + } else if (subject instanceof ARC4Encoded) { + return subject.bytes.equals((test as unknown as ARC4Encoded).bytes) + } else if (Array.isArray(subject)) { + return (test as []).map((x, i) => matchImpl((subject as DeliberateAny)[i], x as DeliberateAny)).every((x) => x) + } else if (typeof subject === 'object') { + return Object.entries(test!) + .map(([k, v]) => matchImpl((subject as DeliberateAny)[k], v as DeliberateAny)) + .every((x) => x) + } + return false +} + +export const assertMatchImpl: typeof assertMatch = (subject, test, message): boolean => { + const isMatching = matchImpl(subject, test) + assert(isMatching, message) + return isMatching +} + +const getBigIntValue = (x: unknown) => { + return asMaybeBigUintCls(x)?.asBigInt() +} diff --git a/src/impl/urange.ts b/src/impl/urange.ts new file mode 100644 index 0000000..647e786 --- /dev/null +++ b/src/impl/urange.ts @@ -0,0 +1,18 @@ +import { internal } from '@algorandfoundation/algorand-typescript' +import { asBigInt, asUint64 } from '../util' + +export function* urangeImpl( + a: internal.primitives.StubUint64Compat, + b?: internal.primitives.StubUint64Compat, + c?: internal.primitives.StubUint64Compat, +) { + const start = b ? asBigInt(a) : BigInt(0) + const end = b ? asBigInt(b) : asBigInt(a) + const step = c ? asBigInt(c) : BigInt(1) + let iterationCount = 0 + for (let i = start; i < end; i += step) { + iterationCount++ + yield asUint64(i) + } + return iterationCount +} diff --git a/src/runtime-helpers.ts b/src/runtime-helpers.ts index b16e975..2edd1b5 100644 --- a/src/runtime-helpers.ts +++ b/src/runtime-helpers.ts @@ -12,7 +12,9 @@ export { emitImpl } from './impl/emit' export * from './impl/encoded-types' export { decodeArc4Impl, encodeArc4Impl } from './impl/encoded-types' export { ensureBudgetImpl } from './impl/ensure-budget' +export { assertMatchImpl, matchImpl } from './impl/match' export { TemplateVarImpl } from './impl/template-var' +export { urangeImpl } from './impl/urange' export function switchableValue(x: unknown): bigint | string | boolean { if (typeof x === 'boolean') return x diff --git a/src/subcontexts/ledger-context.ts b/src/subcontexts/ledger-context.ts index e98688c..9062a48 100644 --- a/src/subcontexts/ledger-context.ts +++ b/src/subcontexts/ledger-context.ts @@ -27,6 +27,13 @@ export class LedgerContext { this.appIdContractMap.set(appId, contract) } + getAccount(address: Account): Account { + if (this.accountDataMap.has(address)) { + return Account(address.bytes) + } + throw internal.errors.internalError('Unknown account, check correct testing context is active') + } + getAsset(assetId: internal.primitives.StubUint64Compat): Asset { if (this.assetDataMap.has(assetId)) { return Asset(asUint64(assetId)) @@ -57,16 +64,16 @@ export class LedgerContext { return undefined } const entries = this.applicationDataMap.entries() - let next = entries.next().value + let next = entries.next() let found = false - while (next && !found) { - found = next[1].application.approvalProgram === approvalProgram + while (!next.done && !found) { + found = next.value[1].application.approvalProgram === approvalProgram if (!found) { - next = entries.next().value + next = entries.next() } } - if (found && next) { - const appId = asUint64(next[0]) + if (found && next?.value) { + const appId = asUint64(next.value[0]) if (this.applicationDataMap.has(appId)) { return Application(appId) } diff --git a/src/test-transformer/visitors.ts b/src/test-transformer/visitors.ts index fe9bf68..8c69489 100644 --- a/src/test-transformer/visitors.ts +++ b/src/test-transformer/visitors.ts @@ -367,7 +367,18 @@ const tryGetStubbedFunctionName = (node: ts.CallExpression, helper: VisitorHelpe if (sourceFileName && !algotsModulePaths.some((s) => sourceFileName.includes(s))) return undefined } const functionName = functionSymbol?.getName() ?? identityExpression.text - const stubbedFunctionNames = ['interpretAsArc4', 'decodeArc4', 'encodeArc4', 'TemplateVar', 'ensureBudget', 'emit', 'compile'] + const stubbedFunctionNames = [ + 'interpretAsArc4', + 'decodeArc4', + 'encodeArc4', + 'TemplateVar', + 'ensureBudget', + 'emit', + 'compile', + 'urange', + 'match', + 'assertMatch', + ] return stubbedFunctionNames.includes(functionName) ? functionName : undefined } diff --git a/tests/match.spec.ts b/tests/match.spec.ts new file mode 100644 index 0000000..aa04660 --- /dev/null +++ b/tests/match.spec.ts @@ -0,0 +1,122 @@ +import { assertMatch, biguint, BigUint, Bytes, match, Uint64 } from '@algorandfoundation/algorand-typescript' +import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing' +import { afterEach, describe, expect, test } from 'vitest' +import { MAX_UINT512, MAX_UINT64 } from '../src/constants' +import { StrImpl } from '../src/impl/encoded-types' +describe('match', () => { + const ctx = new TestExecutionContext() + + afterEach(async () => { + ctx.reset() + }) + + const numericTestData = [ + { subject: 1, test: 1, expected: true }, + { subject: 0, test: 1, expected: false }, + { subject: 1, test: 0, expected: false }, + { subject: 1, test: Uint64(1), expected: true }, + { subject: Uint64(1), test: Uint64(1), expected: true }, + { subject: Uint64(1), test: 1, expected: true }, + { subject: 42, test: MAX_UINT64, expected: false }, + { subject: Uint64(MAX_UINT64), test: Uint64(42), expected: false }, + { subject: BigUint(1), test: 1n, expected: true }, + { subject: 1n, test: BigUint(1), expected: true }, + { subject: BigUint(1), test: BigUint(1), expected: true }, + { subject: 42n, test: MAX_UINT512, expected: false }, + { subject: BigUint(MAX_UINT512), test: BigUint(42n), expected: false }, + { subject: { a: BigUint(MAX_UINT512) }, test: { a: { lessThan: MAX_UINT512 } }, expected: false }, + { subject: { a: BigUint(MAX_UINT512) }, test: { a: { lessThanEq: MAX_UINT64 } }, expected: false }, + { subject: { a: MAX_UINT64 }, test: { a: { lessThan: BigUint(MAX_UINT512) } }, expected: true }, + { subject: { a: MAX_UINT512 }, test: { a: { lessThanEq: BigUint(MAX_UINT512) } }, expected: true }, + { subject: { a: BigUint(MAX_UINT512) }, test: { a: { greaterThan: MAX_UINT512 } }, expected: false }, + { subject: { a: BigUint(MAX_UINT64) }, test: { a: { greaterThanEq: MAX_UINT512 } }, expected: false }, + { subject: { a: MAX_UINT512 }, test: { a: { greaterThan: BigUint(MAX_UINT64) } }, expected: true }, + { subject: { a: MAX_UINT512 }, test: { a: { greaterThanEq: BigUint(MAX_UINT512) } }, expected: true }, + { + subject: { a: MAX_UINT512 }, + test: { a: { between: [BigUint(MAX_UINT64), BigUint(MAX_UINT512)] as [biguint, biguint] } }, + expected: true, + }, + { + subject: { a: MAX_UINT64 }, + test: { a: { between: [BigUint(MAX_UINT64), BigUint(MAX_UINT512)] as [biguint, biguint] } }, + expected: true, + }, + { subject: { a: 42 }, test: { a: { between: [BigUint(MAX_UINT64), BigUint(MAX_UINT512)] as [biguint, biguint] } }, expected: false }, + ] + + const account1 = ctx.any.account() + const sameAccount = ctx.ledger.getAccount(account1) + const differentAccount = ctx.any.account() + + const app1 = ctx.any.application() + const sameApp = ctx.ledger.getApplication(app1.id) + const differentApp = ctx.any.application() + + const asset1 = ctx.any.asset() + const sameAsset = ctx.ledger.getAsset(asset1.id) + const differentAsset = ctx.any.application() + + const arc4Str1 = ctx.any.arc4.str(10) + const sameArc4Str = new StrImpl((arc4Str1 as StrImpl).typeInfo, arc4Str1.native) + const differentArc4Str = ctx.any.arc4.str(10) + + const testData = [ + { subject: '', test: '', expected: true }, + { subject: 'hello', test: 'hello', expected: true }, + { subject: 'hello', test: 'world', expected: false }, + { subject: '', test: 'world', expected: false }, + { subject: Bytes(), test: Bytes(), expected: true }, + { subject: Bytes('hello'), test: Bytes('hello'), expected: true }, + { subject: Bytes('hello'), test: Bytes('world'), expected: false }, + { subject: Bytes(''), test: Bytes('world'), expected: false }, + { subject: account1, test: account1, expected: true }, + { subject: account1, test: sameAccount, expected: true }, + { subject: account1, test: differentAccount, expected: false }, + { subject: app1, test: app1, expected: true }, + { subject: app1, test: sameApp, expected: true }, + { subject: app1, test: differentApp, expected: false }, + { subject: asset1, test: asset1, expected: true }, + { subject: asset1, test: sameAsset, expected: true }, + { subject: asset1, test: differentAsset, expected: false }, + { subject: arc4Str1, test: arc4Str1, expected: true }, + { subject: arc4Str1, test: sameArc4Str, expected: true }, + { subject: arc4Str1, test: differentArc4Str, expected: false }, + { subject: { a: 'hello', b: 42, c: arc4Str1 }, test: { a: 'hello', b: { lessThanEq: 42 }, c: sameArc4Str }, expected: true }, + { subject: { a: 'hello', b: 42, c: arc4Str1 }, test: { c: sameArc4Str }, expected: true }, + { subject: { a: 'hello', b: 42, c: arc4Str1 }, test: { c: differentArc4Str }, expected: false }, + { subject: ['hello', 42, arc4Str1], test: ['hello', { lessThanEq: 42 }, sameArc4Str], expected: true }, + { subject: ['hello', 42, arc4Str1], test: ['hello'], expected: true }, + { subject: ['hello', 42, arc4Str1], test: ['world'], expected: false }, + ] + + test.each(numericTestData)('should be able to match numeric data %s', (data) => { + const { subject, test, expected } = data + expect(match(subject, test)).toBe(expected) + }) + + test.each(testData)('should be able to match %s', (data) => { + const { subject, test, expected } = data + expect(match(subject, test)).toBe(expected) + }) + + test.each(numericTestData.filter((x) => x.expected))('should be able to assert match numeric data %s', (data) => { + const { subject, test, expected } = data + expect(assertMatch(subject, test)).toBe(expected) + }) + + test.each(testData.filter((x) => x.expected))('should be able to assert match %s', (data) => { + const { subject, test, expected } = data + expect(match(subject, test)).toBe(expected) + }) + + test.each(numericTestData.filter((x) => !x.expected))('should throw exception when assert match fails for numeric data %s', (data) => { + const { subject, test } = data + expect(() => assertMatch(subject, test)).toThrow('Assertion failed') + }) + + test.each(testData.filter((x) => !x.expected))('should throw exception when assert match fails %s', (data) => { + const { subject, test } = data + expect(() => assertMatch(subject, test)).toThrow('Assertion failed') + }) +}) diff --git a/tests/urange.spec.ts b/tests/urange.spec.ts new file mode 100644 index 0000000..f68d9ea --- /dev/null +++ b/tests/urange.spec.ts @@ -0,0 +1,57 @@ +import { Uint64, urange } from '@algorandfoundation/algorand-typescript' +import { describe, expect, it } from 'vitest' + +describe('urange', () => { + it('should iterate from 0 to a-1 when only a is provided', () => { + const a = Uint64(5) + const iterator = urange(a) + const result = [] + + for (let item = iterator.next(); !item.done; item = iterator.next()) { + result.push(item.value) + } + + expect(result).toEqual([BigInt(0), BigInt(1), BigInt(2), BigInt(3), BigInt(4)]) + }) + + it('should iterate from a to b-1 when a and b are provided', () => { + const a = Uint64(2) + const b = Uint64(5) + const iterator = urange(a, b) + const result = [] + + for (let item = iterator.next(); !item.done; item = iterator.next()) { + result.push(item.value) + } + + expect(result).toEqual([BigInt(2), BigInt(3), BigInt(4)]) + }) + + it('should iterate from a to b-1 with step c when a, b, and c are provided', () => { + const a = Uint64(2) + const b = Uint64(10) + const c = Uint64(2) + const iterator = urange(a, b, c) + const result = [] + + for (let item = iterator.next(); !item.done; item = iterator.next()) { + result.push(item.value) + } + + expect(result).toEqual([BigInt(2), BigInt(4), BigInt(6), BigInt(8)]) + }) + + it('should return iteration count when done', () => { + const a = Uint64(3) + const iterator = urange(a) + let item = iterator.next() + let count = 0 + + while (!item.done) { + count++ + item = iterator.next() + } + + expect(item.value).toBe(count) + }) +})