From de8c27c0f528738a3377f3e6b4a0ee729d3ce4d8 Mon Sep 17 00:00:00 2001 From: Im-Beast Date: Mon, 14 Oct 2024 17:01:44 +0200 Subject: regression tests --- .github/workflows/regression.yaml | 47 +++++++++++++++ .gitignore | 5 +- impls/monero.ts/README.md | 10 +++- impls/monero.ts/src/wallet.ts | 4 ++ tests/compare.ts | 70 +++++++++++++++++++++++ tests/deno.jsonc | 5 ++ tests/deno.lock | 90 +++++++++++++++++++++++++++++ tests/regression.test.ts | 34 +++++++++++ tests/utils.ts | 116 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 378 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/regression.yaml create mode 100755 tests/compare.ts create mode 100755 tests/deno.jsonc create mode 100755 tests/deno.lock create mode 100755 tests/regression.test.ts create mode 100755 tests/utils.ts diff --git a/.github/workflows/regression.yaml b/.github/workflows/regression.yaml new file mode 100644 index 0000000..b79f4ff --- /dev/null +++ b/.github/workflows/regression.yaml @@ -0,0 +1,47 @@ +name: Check if any regressions have happened +on: [push] +permissions: + issues: write + pull-requests: write +jobs: + regression_check: + strategy: + matrix: + coin: [monero] + runs-on: ubuntu-24.04 + steps: + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y build-essential pkg-config autoconf libtool ccache make cmake gcc g++ git curl lbzip2 gperf gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 gcc-mingw-w64-i686 g++-mingw-w64-i686 wget xz-utils + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Patch sources + run: | + git config --global --add safe.directory '*' + git config --global user.email "ci@mrcyjanek.net" + git config --global user.name "CI mrcyjanek.net" + ./apply_patches.sh ${{ matrix.coin }} + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: ${{ github.job }}-${{ matrix.coin }} + - name: Cache built + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + uses: actions/cache@v4 + with: + path: | + ${{ matrix.coin }}/contrib/depends/built/* + key: depends-${{ github.job }}-${{ matrix.coin }}-${{ hashFiles('*/contrib/depends/packages/*.mk') }} + + - name: Run regression tests + run: deno test -A tests/ diff --git a/.gitignore b/.gitignore index 4d94944..2dbc30e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ release/ -build/ \ No newline at end of file +build/ +tests/monero-cli +tests/libs +tests/wallets diff --git a/impls/monero.ts/README.md b/impls/monero.ts/README.md index e3b20f6..773e8f6 100644 --- a/impls/monero.ts/README.md +++ b/impls/monero.ts/README.md @@ -7,10 +7,16 @@ This library does not ship with `monero_c` libraries.\ To use these bindings you have to bring your own `monero_c` libraries.\ There are at least two ways to do so: + - Ahead-of-time, during builds where you only ship necessary library for a given platform.\ - See [monero-tui](https://github.com/Im-Beast/monero-tui/blob/main/.github/workflows/dev-build.yml) build workflow as an example of doing so. + See [monero-tui](https://github.com/Im-Beast/monero-tui/blob/main/.github/workflows/dev-build.yml) build workflow as + an example of doing so. ```ts - import { loadDylib, Wallet, WalletManager } from "https://raw.githubusercontent.com/MrCyjaneK/monero_c/master/impls/monero.ts/mod.ts"; + import { + loadDylib, + Wallet, + WalletManager, + } from "https://raw.githubusercontent.com/MrCyjaneK/monero_c/master/impls/monero.ts/mod.ts"; // Try to load dylib from the default lib/* path loadDylib(); diff --git a/impls/monero.ts/src/wallet.ts b/impls/monero.ts/src/wallet.ts index 07c40ce..e3aae2e 100644 --- a/impls/monero.ts/src/wallet.ts +++ b/impls/monero.ts/src/wallet.ts @@ -19,6 +19,10 @@ export class Wallet { this.sanitizer = sanitizer; } + getPointer(): WalletPtr { + return this.#walletPtr; + } + async store(path = ""): Promise { const bool = await dylib.symbols.MONERO_Wallet_store(this.#walletPtr, CString(path)); await this.throwIfError(); diff --git a/tests/compare.ts b/tests/compare.ts new file mode 100755 index 0000000..9a8fa8d --- /dev/null +++ b/tests/compare.ts @@ -0,0 +1,70 @@ +import { loadDylib, symbols as bindingsSymbols } from "../impls/monero.ts/src/bindings.ts"; +import { Wallet, WalletManager } from "../impls/monero.ts/mod.ts"; +import { readCString } from "../impls/monero.ts/src/utils.ts"; +import { assertEquals } from "jsr:@std/assert"; + +const version = Deno.args[0]; +const walletInfo = JSON.parse(Deno.args[1]); + +// Those don't exist on older versions of monero_c +// @ts-expect-error - +delete bindingsSymbols.MONERO_checksum_wallet2_api_c_h; +// @ts-expect-error - +delete bindingsSymbols.MONERO_checksum_wallet2_api_c_cpp; +// @ts-expect-error - +delete bindingsSymbols.MONERO_checksum_wallet2_api_c_exp; + +const symbols = { + ...bindingsSymbols, + + "MONERO_Wallet_secretViewKey": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, + "MONERO_Wallet_publicViewKey": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, + + "MONERO_Wallet_secretSpendKey": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, + "MONERO_Wallet_publicSpendKey": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, +} as const; + +const dylib = Deno.dlopen(`tests/libs/${version}/monero_libwallet2_api_c.so`, symbols); +loadDylib(dylib as Deno.DynamicLibrary); + +const walletManager = await WalletManager.new(); +const wallet = await Wallet.open(walletManager, walletInfo.path, walletInfo.password); + +assertEquals(await wallet.address(), walletInfo.address); + +const getKey = async (wallet: Wallet, type: `${"secret" | "public"}${"Spend" | "View"}Key`) => + await readCString( + await dylib.symbols[`MONERO_Wallet_${type}` as const](wallet.getPointer()), + ); + +assertEquals(await getKey(wallet, "publicSpendKey"), walletInfo.publicSpendKey); +assertEquals(await getKey(wallet, "secretSpendKey"), walletInfo.secretSpendKey); + +assertEquals(await getKey(wallet, "publicViewKey"), walletInfo.publicViewKey); +assertEquals(await getKey(wallet, "secretViewKey"), walletInfo.secretViewKey); + +await wallet.store(walletInfo.path); diff --git a/tests/deno.jsonc b/tests/deno.jsonc new file mode 100755 index 0000000..a7b75ec --- /dev/null +++ b/tests/deno.jsonc @@ -0,0 +1,5 @@ +{ + "fmt": { + "lineWidth": 120 + } +} diff --git a/tests/deno.lock b/tests/deno.lock new file mode 100755 index 0000000..02d189f --- /dev/null +++ b/tests/deno.lock @@ -0,0 +1,90 @@ +{ + "version": "4", + "specifiers": { + "jsr:@david/dax@*": "0.42.0", + "jsr:@david/path@0.2": "0.2.0", + "jsr:@david/which@~0.4.1": "0.4.1", + "jsr:@std/assert@*": "0.221.0", + "jsr:@std/assert@0.221": "0.221.0", + "jsr:@std/bytes@0.221": "0.221.0", + "jsr:@std/fmt@0.221": "0.221.0", + "jsr:@std/fmt@1": "1.0.2", + "jsr:@std/fs@1": "1.0.4", + "jsr:@std/io@0.221": "0.221.0", + "jsr:@std/path@1": "1.0.6", + "jsr:@std/path@^1.0.6": "1.0.6", + "jsr:@std/streams@0.221": "0.221.0", + "jsr:@std/streams@^1.0.7": "1.0.7", + "jsr:@std/tar@*": "0.1.2" + }, + "jsr": { + "@david/dax@0.42.0": { + "integrity": "0c547c9a20577a6072b90def194c159c9ddab82280285ebfd8268a4ebefbd80b", + "dependencies": [ + "jsr:@david/path", + "jsr:@david/which", + "jsr:@std/fmt@1", + "jsr:@std/fs", + "jsr:@std/io", + "jsr:@std/path@1", + "jsr:@std/streams@0.221" + ] + }, + "@david/path@0.2.0": { + "integrity": "f2d7aa7f02ce5a55e27c09f9f1381794acb09d328f8d3c8a2e3ab3ffc294dccd", + "dependencies": [ + "jsr:@std/fs", + "jsr:@std/path@1" + ] + }, + "@david/which@0.4.1": { + "integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e" + }, + "@std/assert@0.221.0": { + "integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a", + "dependencies": [ + "jsr:@std/fmt@0.221" + ] + }, + "@std/bytes@0.221.0": { + "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" + }, + "@std/fmt@0.221.0": { + "integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a" + }, + "@std/fmt@1.0.2": { + "integrity": "87e9dfcdd3ca7c066e0c3c657c1f987c82888eb8103a3a3baa62684ffeb0f7a7" + }, + "@std/fs@1.0.4": { + "integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c", + "dependencies": [ + "jsr:@std/path@^1.0.6" + ] + }, + "@std/io@0.221.0": { + "integrity": "faf7f8700d46ab527fa05cc6167f4b97701a06c413024431c6b4d207caa010da", + "dependencies": [ + "jsr:@std/assert@0.221", + "jsr:@std/bytes" + ] + }, + "@std/path@1.0.6": { + "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" + }, + "@std/streams@0.221.0": { + "integrity": "47f2f74634b47449277c0ee79fe878da4424b66bd8975c032e3afdca88986e61", + "dependencies": [ + "jsr:@std/io" + ] + }, + "@std/streams@1.0.7": { + "integrity": "1a93917ca0c58c01b2bfb93647189229b1702677f169b6fb61ad6241cd2e499b" + }, + "@std/tar@0.1.2": { + "integrity": "98183102395decd6268253996177804f818580ef547a25b81da0e7cc334db708", + "dependencies": [ + "jsr:@std/streams@^1.0.7" + ] + } + } +} diff --git a/tests/regression.test.ts b/tests/regression.test.ts new file mode 100755 index 0000000..677f9b6 --- /dev/null +++ b/tests/regression.test.ts @@ -0,0 +1,34 @@ +import { $, createWalletViaCli, downloadMoneroCli, getMoneroC, getMoneroCTags } from "./utils.ts"; + +Deno.test("Regression tests", async (t) => { + await Deno.remove("./tests/wallets", { recursive: true }).catch(() => {}); + await Deno.mkdir("./tests/wallets", { recursive: true }); + + const tags = await getMoneroCTags(); + const latestTag = tags[0]; + await Promise.all([getMoneroC("next"), await getMoneroC(latestTag), downloadMoneroCli()]); + + await t.step("Simple (next, latest, next)", async () => { + const walletInfo = await createWalletViaCli("dog", "sobaka"); + + for (const version of ["next", latestTag, "next"]) { + await $`deno run -A ./tests/compare.ts ${version} ${JSON.stringify(walletInfo)}`; + } + }); + + await t.step("All releases sequentially (all tags in the release order, next)", async () => { + tags.unshift("next"); + + const walletInfo = await createWalletViaCli("cat", "koshka"); + + for (const version of tags.toReversed()) { + if (version !== "next" && version !== tags[0]) await getMoneroC(version); + await $`deno run -A ./tests/compare.ts ${version} ${JSON.stringify(walletInfo)}`; + } + + await Deno.remove("./tests/wallets", { recursive: true }).catch(() => {}); + }); + + await Deno.remove("./tests/wallets", { recursive: true }).catch(() => {}); + await Deno.remove("./tests/libs", { recursive: true }).catch(() => {}); +}); diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100755 index 0000000..3792fd3 --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,116 @@ +import { build$, CommandBuilder } from "jsr:@david/dax"; + +export const $ = build$({ + commandBuilder: new CommandBuilder() + .printCommand(true) + .stdin("inherit") + .stdout("inherit") + .stderr("inherit"), +}); + +export async function downloadMoneroCli() { + const MONERO_CLI_VERSION = "monero-linux-x64-v0.18.3.4"; + const MONERO_WALLET_CLI = + `https://downloads.getmonero.org/cli/${MONERO_CLI_VERSION}.tar.bz2`; + + await $`wget ${MONERO_WALLET_CLI}`; + await $ + .raw`tar -xvf ${MONERO_CLI_VERSION}.tar.bz2 --one-top-level=monero-cli --strip-components=1 -C tests`; + await $.raw`rm ${MONERO_CLI_VERSION}.tar.bz2`; +} + +interface WalletInfo { + path: string; + password: string; + address: string; + secretSpendKey: string; + publicSpendKey: string; + secretViewKey: string; + publicViewKey: string; +} + +export async function createWalletViaCli( + name: string, + password: string, +): Promise { + const path = `./tests/wallets/${name}`; + + await $`./tests/monero-cli/monero-wallet-cli --generate-new-wallet ${path} --password ${password} --mnemonic-language English --command exit` + .stdout("null"); + + const address = + (await $`./tests/monero-cli/monero-wallet-cli --wallet-file ${path} --password ${password} --command address` + .stdinText(`${password}\n`) + .lines()) + .at(-1)! + .split(/\s+/)[1]; + + const retrieveKeys = (lines: string[]) => + lines.slice(-2) + .map((line) => line.split(": ")[1]); + + const [secretSpendKey, publicSpendKey] = retrieveKeys( + await $`./tests/monero-cli/monero-wallet-cli --wallet-file ${path} --password ${password} --command spendkey` + .stdinText(`${password}\n`) + .lines(), + ); + + const [secretViewKey, publicViewKey] = retrieveKeys( + await $`./tests/monero-cli/monero-wallet-cli --wallet-file ${path} --password ${password} --command viewkey` + .stdinText(`${password}\n`) + .lines(), + ); + + return { + path, + password, + address, + secretSpendKey, + publicSpendKey, + secretViewKey, + publicViewKey, + }; +} + +// deno-lint-ignore ban-types +export type MoneroCVersion = "next" | (string & {}); + +export async function getMoneroCTags(): Promise { + return (( + await (await fetch( + "https://api.github.com/repos/MrCyjanek/monero_c/releases", + )).json() + ) as { tag_name: string }[]) + .map(({ tag_name }) => tag_name); +} +export async function getMoneroC(version: MoneroCVersion) { + const triple = "x86_64-linux-gnu"; + const dylibName = "monero_x86_64-linux-gnu_libwallet2_api_c.so"; + const endpointDylibName = "monero_libwallet2_api_c.so"; + const releaseDylibName = dylibName.slice("monero_".length); + + if (version === "next") { + // build current release + await $ + .raw`bash ./build_single.sh monero ${triple} -j${navigator.hardwareConcurrency}`; + + await $.raw`xz -kd release/monero/${releaseDylibName}.xz`; + await $`mkdir -p tests/libs/next`; + await $`mv release/monero/${releaseDylibName} tests/libs/next/${endpointDylibName}`; + } else { + const downloadUrl = + `https://github.com/MrCyjaneK/monero_c/releases/download/${version}/${dylibName}.xz`; + + const file = await Deno.open(`./tests/${dylibName}.xz`, { + create: true, + write: true, + }); + file.write(await (await fetch(downloadUrl)).bytes()); + file.close(); + + await $.raw`xz -d ./tests/${dylibName}.xz`; + await $.raw`mkdir -p ./tests/libs/${version}`; + await $ + .raw`mv ./tests/${dylibName} ./tests/libs/${version}/${endpointDylibName}`; + } +} -- cgit v1.2.3