From 303caf1cf3942e3fa763700717c1710ee5b63319 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:53:52 +0000 Subject: [PATCH] chore: Replace console.warn with @tauri-apps/plugin-log Replaces the raw `console.warn` in the evaluation engine (`src/engine/index.ts`) with the Tauri logging plugin (`warn` from `@tauri-apps/plugin-log`) for better production observability. Ensures tests mock this API. Added necessary backend initialization in `src-tauri`. Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> --- package-lock.json | 10 ++++ package.json | 1 + src-tauri/Cargo.lock | 69 ++++++++++++++++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/main.rs | 1 + src/engine/edge.test.ts | 8 +++- src/engine/engine.test.ts | 74 +++++++++++++++++++++++++----- src/engine/index.ts | 96 ++++++++++++++++++++++++++++++++------- 8 files changed, 230 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb1c03d..3e0c342 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@tauri-apps/plugin-autostart": "^2.3.0", "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-global-shortcut": "^2.2.0", + "@tauri-apps/plugin-log": "^2.9.0", "@tauri-apps/plugin-opener": "^2.2.0", "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-updater": "^2.10.1", @@ -1244,6 +1245,15 @@ "@tauri-apps/api": "^2.11.0" } }, + "node_modules/@tauri-apps/plugin-log": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.9.0.tgz", + "integrity": "sha512-Ql8okrnsguk0eDq1GvRfttFV5KaeW/7vcao6bdbkXCRJ1+2sWE15ZJvJVEKVANrOKy1mRngqC3IFIAP+wP5qSw==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, "node_modules/@tauri-apps/plugin-opener": { "version": "2.5.4", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", diff --git a/package.json b/package.json index 35dd146..c7eee36 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@tauri-apps/plugin-autostart": "^2.3.0", "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-global-shortcut": "^2.2.0", + "@tauri-apps/plugin-log": "^2.9.0", "@tauri-apps/plugin-opener": "^2.2.0", "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-updater": "^2.10.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 149f78d..57ba4c0 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -32,6 +32,23 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -941,6 +958,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1004,6 +1031,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + [[package]] name = "field-offset" version = "0.3.6" @@ -2270,6 +2306,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc2" version = "0.6.4" @@ -3736,6 +3781,7 @@ dependencies = [ "tauri-plugin-autostart", "tauri-plugin-dialog", "tauri-plugin-global-shortcut", + "tauri-plugin-log", "tauri-plugin-opener", "tauri-plugin-process", "tauri-plugin-single-instance", @@ -4075,6 +4121,27 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "tauri-plugin-log" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6792296e6f389268016c77db21ebae1fc0568f2fccf88b1ec7e2ea71330afb4c" +dependencies = [ + "android_logger", + "fern", + "log", + "objc2", + "objc2-foundation", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.4" @@ -4326,7 +4393,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ad42302..e1bc5e1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ serde_json = "1" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } chrono = "0.4" futures = "0.3.32" +tauri-plugin-log = "2.9.0" [profile.release] strip = true diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5f2fc06..0d2c612 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -985,6 +985,7 @@ fn main() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_log::Builder::new().build()) .manage(DroppedFiles::default()) .manage(DataDirGate::default()) .invoke_handler(tauri::generate_handler![ diff --git a/src/engine/edge.test.ts b/src/engine/edge.test.ts index 5ebd457..25fed8a 100644 --- a/src/engine/edge.test.ts +++ b/src/engine/edge.test.ts @@ -1,6 +1,12 @@ -import { describe, it, expect, beforeAll } from "vitest"; +import { describe, it, expect, beforeAll, vi } from "vitest"; import { SumEngine } from "./index"; +vi.mock("@tauri-apps/plugin-log", () => ({ + warn: vi.fn().mockResolvedValue(undefined), + error: vi.fn().mockResolvedValue(undefined), + info: vi.fn().mockResolvedValue(undefined), +})); + let eng: SumEngine; beforeAll(() => { diff --git a/src/engine/engine.test.ts b/src/engine/engine.test.ts index febef3a..3b4433a 100644 --- a/src/engine/engine.test.ts +++ b/src/engine/engine.test.ts @@ -1,5 +1,11 @@ -import { describe, it, expect, beforeAll } from "vitest"; +import { describe, it, expect, beforeAll, vi } from "vitest"; import { SumEngine } from "./index"; + +vi.mock("@tauri-apps/plugin-log", () => ({ + warn: vi.fn().mockResolvedValue(undefined), + error: vi.fn().mockResolvedValue(undefined), + info: vi.fn().mockResolvedValue(undefined), +})); import { qty, EvalError, XRefError } from "./types"; import type { XRefResolution, EvalCtx } from "./evaluator"; import { evaluate } from "./evaluator"; @@ -230,7 +236,13 @@ describe("variables and document context", () => { }); it("sum / total", () => { expect(calcDoc("10\n20\n30\nsum")).toEqual(["10", "20", "30", "60"]); - expect(calcDoc("10\n20\n\n30\nsum")).toEqual(["10", "20", null, "30", "30"]); + expect(calcDoc("10\n20\n\n30\nsum")).toEqual([ + "10", + "20", + null, + "30", + "30", + ]); }); it("sum with units", () => { expect(calcDoc("$10\n$20\ntotal")).toEqual(["$10", "$20", "$30"]); @@ -243,15 +255,31 @@ describe("variables and document context", () => { }); it("count", () => { expect(calcDoc("10\n20\n30\ncount")).toEqual(["10", "20", "30", "3"]); - expect(calcDoc("10\n20\n\n30\ncount")).toEqual(["10", "20", null, "30", "1"]); + expect(calcDoc("10\n20\n\n30\ncount")).toEqual([ + "10", + "20", + null, + "30", + "1", + ]); }); it("min / max", () => { expect(calcDoc("10\n20\n5\nmin")).toEqual(["10", "20", "5", "5"]); expect(calcDoc("10\n20\n5\nmax")).toEqual(["10", "20", "5", "20"]); }); it("min / max with units", () => { - expect(calcDoc("10 m\n5 m\n20 m\nmin")).toEqual(["10 m", "5 m", "20 m", "5 m"]); - expect(calcDoc("10 m\n5 m\n20 m\nmax")).toEqual(["10 m", "5 m", "20 m", "20 m"]); + expect(calcDoc("10 m\n5 m\n20 m\nmin")).toEqual([ + "10 m", + "5 m", + "20 m", + "5 m", + ]); + expect(calcDoc("10 m\n5 m\n20 m\nmax")).toEqual([ + "10 m", + "5 m", + "20 m", + "20 m", + ]); }); it("product", () => { expect(calcDoc("2\n3\n4\nproduct")).toEqual(["2", "3", "4", "24"]); @@ -332,18 +360,32 @@ describe("goal seek", () => { describe("extension API", () => { it("addUnit (horse from Sample.js)", () => { - eng.addUnit({ id: "horse", phrases: "horse, horses, hrs", baseUnitId: "meter", format: "hrs", ratio: 2.4 }); + eng.addUnit({ + id: "horse", + phrases: "horse, horses, hrs", + baseUnitId: "meter", + format: "hrs", + ratio: 2.4, + }); expect(calc("2 horses in meters")).toBe("4.8 m"); }); it("addUnit throws on id collision instead of silently corrupting unitsById", () => { expect(() => - eng.addUnit({ id: "horse", phrases: "nag", baseUnitId: "meter", format: "nag", ratio: 99 }) + eng.addUnit({ + id: "horse", + phrases: "nag", + baseUnitId: "meter", + format: "nag", + ratio: 99, + }), ).toThrow(/horse/); // the original registration must survive the failed collision expect(calc("2 horses in meters")).toBe("4.8 m"); }); it("addFunction (zum from Sample.js)", () => { - eng.addFunction({ id: "zum", phrases: "zum" }, (values) => ({ double: values[0].double + values[1].double })); + eng.addFunction({ id: "zum", phrases: "zum" }, (values) => ({ + double: values[0].double + values[1].double, + })); expect(calc("zum(2;3)")).toBe("5"); }); it("setVariable", () => { @@ -374,7 +416,8 @@ describe("cross-sheet references", () => { const s = sheet.toLowerCase(); if (s === "budget") { if (key === "total") return { ok: true, value: qty(150) }; - if (key === "rent") return { ok: true, value: qty(500, eng.reg.unitsById.get("meter")!) }; + if (key === "rent") + return { ok: true, value: qty(500, eng.reg.unitsById.get("meter")!) }; if (key === "last") return { ok: true, value: qty(7) }; return { ok: false, reason: `no variable "${key}" in "${sheet}"` }; } @@ -415,7 +458,10 @@ describe("cross-sheet references", () => { expect(r[0].error).toBeUndefined(); }); it("propagates a resolver-reported circular reference", () => { - const cyc = (): XRefResolution => ({ ok: false, reason: "circular reference" }); + const cyc = (): XRefResolution => ({ + ok: false, + reason: "circular reference", + }); const r = eng.evaluateDocument("@A.total", cyc); expect(r[0].value).toBeNull(); expect(r[0].error).toBe("circular reference"); @@ -439,7 +485,9 @@ describe("evaluation errors", () => { reg: eng.reg, historicalRates: {}, } as unknown as EvalCtx; - expect(() => evaluate({ k: "var", name: "unknown" }, ctx)).toThrowError(EvalError); + expect(() => evaluate({ k: "var", name: "unknown" }, ctx)).toThrowError( + EvalError, + ); }); it("throws XRefError for unresolved xref", () => { @@ -449,6 +497,8 @@ describe("evaluation errors", () => { historicalRates: {}, resolveXRef: () => ({ ok: false, reason: "xref error" }), } as unknown as EvalCtx; - expect(() => evaluate({ k: "xref", sheet: "Sheet", key: "key" }, ctx)).toThrowError(XRefError); + expect(() => + evaluate({ k: "xref", sheet: "Sheet", key: "key" }, ctx), + ).toThrowError(XRefError); }); }); diff --git a/src/engine/index.ts b/src/engine/index.ts index 5d6e82e..c33e262 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -1,6 +1,18 @@ // Public facade: evaluates whole documents line by line and exposes the // extension API (numi.addUnit / addFunction / setVariable). -import { DateOrder, Decimal, EngineSettings, EvalError, Quantity, Unit, Value, XRefError, defaultSettings, qty } from "./types"; +import { warn } from "@tauri-apps/plugin-log"; +import { + DateOrder, + Decimal, + EngineSettings, + EvalError, + Quantity, + Unit, + Value, + XRefError, + defaultSettings, + qty, +} from "./types"; import { Registry, buildRegistry, Completion } from "./registry"; import { tokenize, Token } from "./tokenizer"; import { parseLine, ParsedLine } from "./parser"; @@ -84,7 +96,8 @@ export class SumEngine { updateSettings(patch: Partial): void { this.settings = { ...this.settings, ...patch }; - if (patch.dateFormat) this.dateOrder = resolveDateOrder(this.settings.dateFormat); + if (patch.dateFormat) + this.dateOrder = resolveDateOrder(this.settings.dateFormat); } completions(): Completion[] { @@ -106,13 +119,25 @@ export class SumEngine { if (trimmed.length === 0) { const kind = commentStart >= 0 ? "comment" : "empty"; - results.push({ text: null, value: null, kind, tokens: [], commentStart: commentStart >= 0 ? commentStart : null }); + results.push({ + text: null, + value: null, + kind, + tokens: [], + commentStart: commentStart >= 0 ? commentStart : null, + }); lineValues.push(null); lineKinds.push("empty"); continue; } if (trimmed.startsWith("#")) { - results.push({ text: null, value: null, kind: "header", tokens: [], commentStart: commentStart >= 0 ? commentStart : null }); + results.push({ + text: null, + value: null, + kind: "header", + tokens: [], + commentStart: commentStart >= 0 ? commentStart : null, + }); lineValues.push(null); lineKinds.push("header"); continue; @@ -148,7 +173,9 @@ export class SumEngine { } else { // Surface unexpected bugs/BigInt errors/RangeErrors and log them lineError = e instanceof Error ? e.message : String(e); - console.warn("evaluate failed:", e); + warn( + `evaluate failed: ${e instanceof Error ? e.stack || e.message : String(e)}`, + ).catch((err) => console.warn("logger failed", err)); } value = null; } @@ -158,7 +185,10 @@ export class SumEngine { lineValues.push(value); lineKinds.push("normal"); results.push({ - text: (value && value.kind !== "chart") ? formatValue(value, this.settings) : null, + text: + value && value.kind !== "chart" + ? formatValue(value, this.settings) + : null, value, kind: "normal", tokens, @@ -216,9 +246,13 @@ export class SumEngine { } addUnit(spec: ExtensionUnitSpec): void { - const base = this.reg.unitsById.get(spec.baseUnitId) ?? this.currencyBase(spec.baseUnitId); - if (!base) throw new Error(`numi.addUnit: unknown baseUnitId ${spec.baseUnitId}`); - if (this.reg.unitsById.has(spec.id)) throw new Error(`numi.addUnit: id "${spec.id}" is already registered`); + const base = + this.reg.unitsById.get(spec.baseUnitId) ?? + this.currencyBase(spec.baseUnitId); + if (!base) + throw new Error(`numi.addUnit: unknown baseUnitId ${spec.baseUnitId}`); + if (this.reg.unitsById.has(spec.id)) + throw new Error(`numi.addUnit: id "${spec.id}" is already registered`); const unit: Unit = { id: spec.id, dimension: base.dimension, @@ -229,12 +263,24 @@ export class SumEngine { this.reg.unitsById.set(unit.id, unit); for (const p of spec.phrases.split(",")) { const phrase = p.trim(); - if (phrase) this.reg.addPhrase(phrase, { t: "unit", unit }, { caseSensitive: false }); + if (phrase) + this.reg.addPhrase( + phrase, + { t: "unit", unit }, + { caseSensitive: false }, + ); } - this.reg.completions.push({ label: spec.id, type: "unit", detail: "extension" }); + this.reg.completions.push({ + label: spec.id, + type: "unit", + detail: "extension", + }); } - addFunction(spec: { id: string; phrases: string }, fn: (values: ExtensionValue[]) => ExtensionValue | number): void { + addFunction( + spec: { id: string; phrases: string }, + fn: (values: ExtensionValue[]) => ExtensionValue | number, + ): void { const name = spec.id; this.reg.customFuncs.set(name, (args: Value[]) => { const mapped = args.map((a) => this.fromValue(a)); @@ -242,26 +288,42 @@ export class SumEngine { }); for (const p of (spec.phrases || spec.id).split(",")) { const phrase = p.trim(); - if (phrase) this.reg.addPhrase(phrase, { t: "func", name }, { caseSensitive: false }); + if (phrase) + this.reg.addPhrase( + phrase, + { t: "func", name }, + { caseSensitive: false }, + ); } - this.reg.completions.push({ label: name, type: "function", detail: "extension" }); + this.reg.completions.push({ + label: name, + type: "function", + detail: "extension", + }); } private currencyBase(code: string): Unit | null { - return this.reg.currencyCodes.has(code) ? this.reg.makeCurrencyUnit(code) : null; + return this.reg.currencyCodes.has(code) + ? this.reg.makeCurrencyUnit(code) + : null; } private toValue(v: number | ExtensionValue): Value { if (typeof v === "number") return qty(v); const unit = v.unitId - ? this.reg.unitsById.get(v.unitId) ?? this.currencyBase(v.unitId) ?? null + ? (this.reg.unitsById.get(v.unitId) ?? + this.currencyBase(v.unitId) ?? + null) : null; return qty(new Decimal(v.double), unit); } private fromValue(v: Value): ExtensionValue { if (v.kind === "quantity") { - return { double: v.value.toNumber(), ...(v.unit ? { unitId: v.unit.id } : {}) }; + return { + double: v.value.toNumber(), + ...(v.unit ? { unitId: v.unit.id } : {}), + }; } if (v.kind === "percent") return { double: v.value.div(100).toNumber() }; if (v.kind === "date") return { double: v.ms };