Skip to content

Commit

Permalink
Merge pull request #21 from algorandfoundation/feat-utils
Browse files Browse the repository at this point in the history
feat: implement stubs for urange, assertMatch and match functions
  • Loading branch information
boblat authored Dec 30, 2024
2 parents f08cd9f + cd7b7a2 commit 965d07c
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 7 deletions.
56 changes: 56 additions & 0 deletions src/impl/match.ts
Original file line number Diff line number Diff line change
@@ -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()
}
18 changes: 18 additions & 0 deletions src/impl/urange.ts
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions src/runtime-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions src/subcontexts/ledger-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
}
Expand Down
13 changes: 12 additions & 1 deletion src/test-transformer/visitors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
122 changes: 122 additions & 0 deletions tests/match.spec.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
57 changes: 57 additions & 0 deletions tests/urange.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})

0 comments on commit 965d07c

Please sign in to comment.