@harmoniclabs/pebble-cli 0.1.10 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compile/completeCompileOptions.js +2 -0
- package/dist/export/completeExportOptions.js +2 -0
- package/dist/index.js +17 -0
- package/dist/init/initPebbleProject.js +1 -0
- package/dist/repl/pebbleRepl.js +2 -1
- package/dist/test/discoverTestFiles.d.ts +9 -0
- package/dist/test/discoverTestFiles.js +47 -0
- package/dist/test/formatTestResults.d.ts +11 -0
- package/dist/test/formatTestResults.js +79 -0
- package/dist/test/runTestsCommand.d.ts +8 -0
- package/dist/test/runTestsCommand.js +69 -0
- package/dist/version.generated.d.ts +3 -3
- package/dist/version.generated.js +3 -3
- package/package.json +2 -2
|
@@ -5,6 +5,8 @@ import { normalizeRoot, isRecord } from "../utils/miscellaneous.js";
|
|
|
5
5
|
export function completeCompileOptions(flags) {
|
|
6
6
|
const root = normalizeRoot();
|
|
7
7
|
const configPath = path.resolve(root, flags.config ?? "./pebble.config.json");
|
|
8
|
+
// compilerVersion is intentionally not set here — it must come from the
|
|
9
|
+
// user's pebble.config.json. If missing/invalid, the Compiler throws.
|
|
8
10
|
let config = productionOptions;
|
|
9
11
|
if (existsSync(configPath)) {
|
|
10
12
|
try {
|
|
@@ -8,6 +8,8 @@ export function completeExportOptions(flags) {
|
|
|
8
8
|
throw new Error("exported function name must be provided via '--function-name <name>' flag");
|
|
9
9
|
const functionName = flags.functionName.trim();
|
|
10
10
|
const configPath = path.resolve(root, flags.config ?? "./pebble.config.json");
|
|
11
|
+
// compilerVersion is intentionally not set here — it must come from the
|
|
12
|
+
// user's pebble.config.json. If missing/invalid, the Compiler throws.
|
|
11
13
|
let config = productionOptions;
|
|
12
14
|
if (existsSync(configPath)) {
|
|
13
15
|
try {
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { completeExportOptions } from "./export/completeExportOptions.js";
|
|
|
8
8
|
import { exportPebbleFunction } from "./export/exportPebbleFunction.js";
|
|
9
9
|
import { prettyPrintUplcFromFile } from "./uplc/pretty/prettyPrintUplcFromFile.js";
|
|
10
10
|
import { pebbleRepl } from "./repl/pebbleRepl.js";
|
|
11
|
+
import { runTestsCommand } from "./test/runTestsCommand.js";
|
|
11
12
|
const program = new Command();
|
|
12
13
|
const versionOutput = (`pebble-cli version: ${PEBBLE_VERSION}
|
|
13
14
|
pebble language version: ${PEBBLE_LIB_VERSION}
|
|
@@ -78,4 +79,20 @@ defineVersionManager( program );
|
|
|
78
79
|
program.command("repl")
|
|
79
80
|
.description("Start an interactive Pebble REPL")
|
|
80
81
|
.action(pebbleRepl);
|
|
82
|
+
program.command("test [path]")
|
|
83
|
+
.description("Discover and run `test name() { ... }` blocks under [path] (default: current directory). Reports execution costs (cpu/mem) for each test. Property tests (params present) sample N iterations.")
|
|
84
|
+
.option("-c, --config <string>", "The config file path", "./pebble.config.json")
|
|
85
|
+
.option("--testPathPattern <regex>", "Run tests only in files whose path matches this regex")
|
|
86
|
+
.option("-t, --testNamePattern <regex>", "Run only tests whose name matches this regex")
|
|
87
|
+
.option("--property-runs <n>", "Number of iterations per property test (default 100)")
|
|
88
|
+
.option("--seed <int>", "Seed for the property-test PRNG (default 0)")
|
|
89
|
+
.action(async (target, opts) => {
|
|
90
|
+
await runTestsCommand(target, {
|
|
91
|
+
config: opts.config,
|
|
92
|
+
testPathPattern: opts.testPathPattern,
|
|
93
|
+
testNamePattern: opts.testNamePattern,
|
|
94
|
+
propertyRuns: opts.propertyRuns,
|
|
95
|
+
seed: opts.seed,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
81
98
|
program.parse();
|
|
@@ -150,6 +150,7 @@ ${includeOffchain ? "- `offchain/`: optional offchain scaffolding\n" : ""}
|
|
|
150
150
|
// pebble config
|
|
151
151
|
const configPath = path.join(projectRoot, "pebble.config.json");
|
|
152
152
|
const pebbleConfig = {
|
|
153
|
+
compilerVersion: "^0.2.0",
|
|
153
154
|
entry: "./src/index.pebble",
|
|
154
155
|
outDir: "./out",
|
|
155
156
|
removeTraces: true,
|
package/dist/repl/pebbleRepl.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as readline from "node:readline/promises";
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { Compiler, createMemoryCompilerIoApi, defaultOptions, fromUtf8, toHex, ConstTyTag, constTypeToStirng, } from "@harmoniclabs/pebble";
|
|
4
|
+
import { Compiler, COMPILER_VERSION, createMemoryCompilerIoApi, defaultOptions, fromUtf8, toHex, ConstTyTag, constTypeToStirng, } from "@harmoniclabs/pebble";
|
|
5
5
|
function formatCEKValue(val) {
|
|
6
6
|
if (val.tag === 5 /* CEKValueTag.Error */) {
|
|
7
7
|
return chalk.red("error") + (val.msg ? `: ${val.msg}` : "");
|
|
@@ -264,6 +264,7 @@ export async function pebbleRepl() {
|
|
|
264
264
|
});
|
|
265
265
|
const compiler = new Compiler(ioApi, {
|
|
266
266
|
...defaultOptions,
|
|
267
|
+
compilerVersion: COMPILER_VERSION,
|
|
267
268
|
silent: true,
|
|
268
269
|
});
|
|
269
270
|
try {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover `.pebble` files to feed into `Compiler.test()`.
|
|
3
|
+
*
|
|
4
|
+
* @param target Either a directory to walk recursively, or a single `.pebble`
|
|
5
|
+
* file path. If omitted, walks `cwd`.
|
|
6
|
+
* @param testPathPattern Optional regex filter over workspace-relative paths.
|
|
7
|
+
* @returns absolute paths of matching `.pebble` files.
|
|
8
|
+
*/
|
|
9
|
+
export declare function discoverTestFiles(target: string | undefined, testPathPattern: RegExp | undefined, cwd?: string): Promise<string[]>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import * as fsp from "node:fs/promises";
|
|
3
|
+
import { existsSync, statSync } from "node:fs";
|
|
4
|
+
/**
|
|
5
|
+
* Discover `.pebble` files to feed into `Compiler.test()`.
|
|
6
|
+
*
|
|
7
|
+
* @param target Either a directory to walk recursively, or a single `.pebble`
|
|
8
|
+
* file path. If omitted, walks `cwd`.
|
|
9
|
+
* @param testPathPattern Optional regex filter over workspace-relative paths.
|
|
10
|
+
* @returns absolute paths of matching `.pebble` files.
|
|
11
|
+
*/
|
|
12
|
+
export async function discoverTestFiles(target, testPathPattern, cwd = process.cwd()) {
|
|
13
|
+
const resolved = path.resolve(cwd, target ?? ".");
|
|
14
|
+
if (!existsSync(resolved))
|
|
15
|
+
return [];
|
|
16
|
+
const stat = statSync(resolved);
|
|
17
|
+
const files = [];
|
|
18
|
+
if (stat.isFile()) {
|
|
19
|
+
if (resolved.endsWith(".pebble"))
|
|
20
|
+
files.push(resolved);
|
|
21
|
+
}
|
|
22
|
+
else if (stat.isDirectory()) {
|
|
23
|
+
await _walk(resolved, files);
|
|
24
|
+
}
|
|
25
|
+
if (testPathPattern) {
|
|
26
|
+
return files.filter(f => testPathPattern.test(path.relative(cwd, f)));
|
|
27
|
+
}
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
async function _walk(dir, out) {
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
if (e.name === "node_modules" || e.name.startsWith("."))
|
|
40
|
+
continue;
|
|
41
|
+
const full = path.join(dir, e.name);
|
|
42
|
+
if (e.isDirectory())
|
|
43
|
+
await _walk(full, out);
|
|
44
|
+
else if (e.isFile() && e.name.endsWith(".pebble"))
|
|
45
|
+
out.push(full);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TestResult } from "@harmoniclabs/pebble";
|
|
2
|
+
export interface FormattedSummary {
|
|
3
|
+
totalTests: number;
|
|
4
|
+
passed: number;
|
|
5
|
+
failed: number;
|
|
6
|
+
skipped: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function formatTestResults(resultsByFile: Map<string, TestResult[]>, cwd?: string): {
|
|
9
|
+
text: string;
|
|
10
|
+
summary: FormattedSummary;
|
|
11
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
export function formatTestResults(resultsByFile, cwd = process.cwd()) {
|
|
3
|
+
const lines = [];
|
|
4
|
+
let passed = 0;
|
|
5
|
+
let failed = 0;
|
|
6
|
+
let skipped = 0;
|
|
7
|
+
let total = 0;
|
|
8
|
+
const files = Array.from(resultsByFile.keys()).sort();
|
|
9
|
+
for (const file of files) {
|
|
10
|
+
const rel = path.relative(cwd, file);
|
|
11
|
+
const results = resultsByFile.get(file);
|
|
12
|
+
lines.push("");
|
|
13
|
+
lines.push(rel);
|
|
14
|
+
for (const r of results) {
|
|
15
|
+
total++;
|
|
16
|
+
if (r.skippedReason) {
|
|
17
|
+
skipped++;
|
|
18
|
+
lines.push(` SKIP ${r.name} (${r.skippedReason})`);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const tag = r.passed ? "PASS" : "FAIL";
|
|
22
|
+
if (r.passed)
|
|
23
|
+
passed++;
|
|
24
|
+
else
|
|
25
|
+
failed++;
|
|
26
|
+
const cpu = r.totalBudget.cpu.toString();
|
|
27
|
+
const mem = r.totalBudget.mem.toString();
|
|
28
|
+
if (r.kind === "property") {
|
|
29
|
+
const ran = r.iterations.length;
|
|
30
|
+
const seedStr = `seed=${r.seed ?? 0}`;
|
|
31
|
+
if (r.passed) {
|
|
32
|
+
lines.push(` ${tag} ${r.name} (${ran} iterations, ${seedStr}, total cpu=${cpu}, mem=${mem})`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const failedIter = r.iterations[r.iterations.length - 1];
|
|
36
|
+
lines.push(` ${tag} ${r.name} (failed at iteration ${ran}, ${seedStr})`);
|
|
37
|
+
if (failedIter?.inputs && failedIter.inputs.length > 0) {
|
|
38
|
+
const inp = failedIter.inputs
|
|
39
|
+
.map(i => `${i.name}=${_renderValue(i.value)}`)
|
|
40
|
+
.join(", ");
|
|
41
|
+
lines.push(` inputs: ${inp}`);
|
|
42
|
+
}
|
|
43
|
+
if (failedIter?.error?.msg)
|
|
44
|
+
lines.push(` error: ${failedIter.error.msg}`);
|
|
45
|
+
if (failedIter?.logs && failedIter.logs.length > 0) {
|
|
46
|
+
for (const log of failedIter.logs)
|
|
47
|
+
lines.push(` trace: ${log}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// unit test rendering
|
|
53
|
+
lines.push(` ${tag} ${r.name} [cpu: ${cpu}, mem: ${mem}]`);
|
|
54
|
+
for (const it of r.iterations) {
|
|
55
|
+
if (it.logs.length > 0) {
|
|
56
|
+
for (const log of it.logs)
|
|
57
|
+
lines.push(` trace: ${log}`);
|
|
58
|
+
}
|
|
59
|
+
if (it.error?.msg)
|
|
60
|
+
lines.push(` error: ${it.error.msg}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
lines.push("");
|
|
65
|
+
lines.push(`Tests: ${passed} passed, ${failed} failed, ${skipped} skipped, ${total} total`);
|
|
66
|
+
return {
|
|
67
|
+
text: lines.join("\n"),
|
|
68
|
+
summary: { totalTests: total, passed, failed, skipped }
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function _renderValue(v) {
|
|
72
|
+
if (typeof v === "bigint")
|
|
73
|
+
return v.toString();
|
|
74
|
+
if (typeof v === "boolean")
|
|
75
|
+
return v ? "true" : "false";
|
|
76
|
+
if (v instanceof Uint8Array)
|
|
77
|
+
return "#" + Array.from(v).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
78
|
+
return String(v);
|
|
79
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { Compiler, productionOptions } from "@harmoniclabs/pebble";
|
|
4
|
+
import { createFsIo } from "../utils/crateFsIo.js";
|
|
5
|
+
import { normalizeRoot, isRecord } from "../utils/miscellaneous.js";
|
|
6
|
+
import { discoverTestFiles } from "./discoverTestFiles.js";
|
|
7
|
+
import { formatTestResults } from "./formatTestResults.js";
|
|
8
|
+
export async function runTestsCommand(target, flags) {
|
|
9
|
+
const root = normalizeRoot();
|
|
10
|
+
// load pebble.config.json if present, otherwise use defaults
|
|
11
|
+
const configPath = path.resolve(root, flags.config ?? "./pebble.config.json");
|
|
12
|
+
// compilerVersion is intentionally not set here — it must come from the
|
|
13
|
+
// user's pebble.config.json. If missing/invalid, the Compiler throws.
|
|
14
|
+
let baseConfig = productionOptions;
|
|
15
|
+
if (existsSync(configPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const txt = readFileSync(configPath, "utf8");
|
|
18
|
+
const parsed = JSON.parse(txt);
|
|
19
|
+
if (isRecord(parsed))
|
|
20
|
+
baseConfig = {
|
|
21
|
+
...productionOptions,
|
|
22
|
+
...parsed,
|
|
23
|
+
uplcOptimizations: {
|
|
24
|
+
...productionOptions.uplcOptimizations,
|
|
25
|
+
...parsed.uplcOptimizations
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// ignore malformed config
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const testPathPattern = flags.testPathPattern ? new RegExp(flags.testPathPattern) : undefined;
|
|
34
|
+
const nameFilter = flags.testNamePattern ? new RegExp(flags.testNamePattern) : undefined;
|
|
35
|
+
const propertyIterations = flags.propertyRuns !== undefined ? Math.max(1, Number(flags.propertyRuns) | 0) : undefined;
|
|
36
|
+
const seed = flags.seed !== undefined ? (Number(flags.seed) | 0) : undefined;
|
|
37
|
+
const files = await discoverTestFiles(target, testPathPattern, root);
|
|
38
|
+
if (files.length === 0) {
|
|
39
|
+
process.stdout.write("no .pebble test files found\n");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const io = createFsIo(root);
|
|
43
|
+
const resultsByFile = new Map();
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const compiler = new Compiler(io, {
|
|
46
|
+
...baseConfig,
|
|
47
|
+
root,
|
|
48
|
+
entry: file,
|
|
49
|
+
silent: true,
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
const results = await compiler.test({
|
|
53
|
+
nameFilter,
|
|
54
|
+
propertyIterations,
|
|
55
|
+
seed,
|
|
56
|
+
});
|
|
57
|
+
if (results.length > 0)
|
|
58
|
+
resultsByFile.set(file, results);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
process.stderr.write(`error running tests in ${path.relative(root, file)}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const { text, summary } = formatTestResults(resultsByFile, root);
|
|
66
|
+
process.stdout.write(text + "\n");
|
|
67
|
+
if (summary.failed > 0)
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const PEBBLE_VERSION = "0.
|
|
2
|
-
export declare const PEBBLE_LIB_VERSION = "0.
|
|
3
|
-
export declare const PEBBLE_COMMIT_HASH = "
|
|
1
|
+
export declare const PEBBLE_VERSION = "0.3.0";
|
|
2
|
+
export declare const PEBBLE_LIB_VERSION = "0.3.0";
|
|
3
|
+
export declare const PEBBLE_COMMIT_HASH = "a7fcaead80e58be72b0c18c877c668a7d630b57c";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// This file is auto-generated by scripts/genVersions.js. Do not edit.
|
|
2
|
-
export const PEBBLE_VERSION = "0.
|
|
3
|
-
export const PEBBLE_LIB_VERSION = "0.
|
|
4
|
-
export const PEBBLE_COMMIT_HASH = "
|
|
2
|
+
export const PEBBLE_VERSION = "0.3.0";
|
|
3
|
+
export const PEBBLE_LIB_VERSION = "0.3.0";
|
|
4
|
+
export const PEBBLE_COMMIT_HASH = "a7fcaead80e58be72b0c18c877c668a7d630b57c";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harmoniclabs/pebble-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A simple, yet rock solid, functional language with an imperative bias, targeting UPLC",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pebble": "./dist/index.js",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@harmoniclabs/crypto": "^0.3.0",
|
|
53
53
|
"@harmoniclabs/obj-utils": "^1.0.0",
|
|
54
|
-
"@harmoniclabs/pebble": "0.
|
|
54
|
+
"@harmoniclabs/pebble": "0.3.0",
|
|
55
55
|
"@harmoniclabs/plutus-machine": "^3.0.0",
|
|
56
56
|
"@harmoniclabs/uint8array-utils": "^1.0.4",
|
|
57
57
|
"@harmoniclabs/uplc": "^2.0.5",
|