@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.
@@ -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,
@@ -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,8 @@
1
+ export interface CliTestFlags {
2
+ config?: string;
3
+ testPathPattern?: string;
4
+ testNamePattern?: string;
5
+ propertyRuns?: string;
6
+ seed?: string;
7
+ }
8
+ export declare function runTestsCommand(target: string | undefined, flags: CliTestFlags): Promise<void>;
@@ -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.1.10";
2
- export declare const PEBBLE_LIB_VERSION = "0.1.10";
3
- export declare const PEBBLE_COMMIT_HASH = "cd5e87c474bea32a9437f9ee81adc0dcca786d0d";
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.1.10";
3
- export const PEBBLE_LIB_VERSION = "0.1.10";
4
- export const PEBBLE_COMMIT_HASH = "cd5e87c474bea32a9437f9ee81adc0dcca786d0d";
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.1.10",
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.1.10",
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",