From ef823836c80ceb5511fa2b3ebeb5a9205017f887 Mon Sep 17 00:00:00 2001 From: Sebastian Herrlinger Date: Mon, 17 Nov 2025 09:56:55 +0100 Subject: [PATCH 1/6] setup test matrix --- .github/workflows/build-core.yml | 63 +++++++++++++++++++-- packages/core/src/lib/tree-sitter/client.ts | 3 +- packages/core/src/zig/renderer.zig | 41 ++++++++++---- 3 files changed, 90 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-core.yml b/.github/workflows/build-core.yml index 6464254a2..0e3b108c4 100644 --- a/.github/workflows/build-core.yml +++ b/.github/workflows/build-core.yml @@ -7,8 +7,8 @@ on: jobs: build: - name: Core - Build and Test - runs-on: ubuntu-latest + name: Build Core Library + runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@v4 @@ -31,7 +31,62 @@ jobs: cd packages/core bun run build - - name: Run tests + - name: Run native tests run: | cd packages/core - bun run test + bun run test:native + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + packages/core/dist + packages/core/node_modules/@opentui/core-* + retention-days: 1 + + test: + name: Test - ${{ matrix.os }} (${{ matrix.arch }}) + needs: build + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - os: macos + arch: arm64 + runner: macos-latest + - os: macos + arch: x64 + runner: macos-13 + - os: linux + arch: x64 + runner: ubuntu-latest + - os: linux + arch: arm64 + runner: ubuntu-24.04-arm + - os: windows + arch: x64 + runner: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: packages/core + + - name: Install dependencies + run: bun install + + - name: Run TypeScript tests + run: | + cd packages/core + bun run test:js diff --git a/packages/core/src/lib/tree-sitter/client.ts b/packages/core/src/lib/tree-sitter/client.ts index 2cb702c43..c3306d51c 100644 --- a/packages/core/src/lib/tree-sitter/client.ts +++ b/packages/core/src/lib/tree-sitter/client.ts @@ -12,10 +12,9 @@ import type { SimpleHighlight, } from "./types" import { getParsers } from "./default-parsers" -import { resolve, isAbsolute } from "path" +import { resolve, isAbsolute, parse } from "path" import { existsSync } from "fs" import { registerEnvVar, env } from "../env" -import { parse } from "path" registerEnvVar({ name: "OTUI_TREE_SITTER_WORKER_PATH", diff --git a/packages/core/src/zig/renderer.zig b/packages/core/src/zig/renderer.zig index f3f921105..84b45f8f8 100644 --- a/packages/core/src/zig/renderer.zig +++ b/packages/core/src/zig/renderer.zig @@ -5,6 +5,7 @@ const buf = @import("buffer.zig"); const gp = @import("grapheme.zig"); const Terminal = @import("terminal.zig"); const logger = @import("logger.zig"); +const builtin = @import("builtin"); pub const RGBA = ansi.RGBA; pub const OptimizedBuffer = buf.OptimizedBuffer; @@ -39,6 +40,32 @@ pub const DebugOverlayCorner = enum { bottomRight, }; +const StdoutWriter = union(enum) { + real: std.io.BufferedWriter(4096, std.fs.File.Writer), + null: void, + + pub fn writer(self: *StdoutWriter) Writer { + return .{ .context = self }; + } + + pub fn flush(self: *StdoutWriter) !void { + switch (self.*) { + .real => |*w| try w.flush(), + .null => {}, + } + } + + const WriteError = std.fs.File.WriteError; + const Writer = std.io.Writer(*StdoutWriter, WriteError, write); + + fn write(self: *StdoutWriter, data: []const u8) WriteError!usize { + switch (self.*) { + .real => |*w| return w.writer().write(data), + .null => return data.len, + } + } +}; + pub const CliRenderer = struct { width: u32, height: u32, @@ -80,7 +107,7 @@ pub const CliRenderer = struct { lastRenderTime: i64, allocator: Allocator, renderThread: ?std.Thread = null, - stdoutWriter: std.io.BufferedWriter(4096, std.fs.File.Writer), + stdoutWriter: StdoutWriter, debugOverlay: struct { enabled: bool, corner: DebugOverlayCorner, @@ -141,17 +168,9 @@ pub const CliRenderer = struct { const currentBuffer = try OptimizedBuffer.init(allocator, width, height, .{ .pool = pool, .width_method = .unicode, .id = "current buffer" }, graphemes_data, display_width); const nextBuffer = try OptimizedBuffer.init(allocator, width, height, .{ .pool = pool, .width_method = .unicode, .id = "next buffer" }, graphemes_data, display_width); - const stdoutWriter = if (testing) blk: { - // In testing mode, use /dev/null to discard output - const devnull = std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }) catch { - // Fallback to stdout if /dev/null can't be opened - logger.warn("Failed to open /dev/null, falling back to stdout\n", .{}); - break :blk std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = std.io.getStdOut().writer() }; - }; - break :blk std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = devnull.writer() }; - } else blk: { + const stdoutWriter: StdoutWriter = if (testing) .{ .null = {} } else blk: { const stdout = std.io.getStdOut(); - break :blk std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = stdout.writer() }; + break :blk .{ .real = std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = stdout.writer() } }; }; // stat sample arrays From 54db19e2bc97e1343de0671db398438c61247bdd Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:30:17 +1000 Subject: [PATCH 2/6] fix(test): Make a truely global invalid path --- packages/core/src/lib/tree-sitter/cache.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/lib/tree-sitter/cache.test.ts b/packages/core/src/lib/tree-sitter/cache.test.ts index fefdc0426..9a41a7b97 100644 --- a/packages/core/src/lib/tree-sitter/cache.test.ts +++ b/packages/core/src/lib/tree-sitter/cache.test.ts @@ -212,7 +212,7 @@ describe("TreeSitterClient Caching", () => { }) test("should handle directory creation errors gracefully", async () => { - const invalidDataPath = "/invalid/path/that/cannot/be/created" + const invalidDataPath = "/invalid\x00/path/with/null/byte" const client = new TreeSitterClient({ dataPath: invalidDataPath }) await expect(client.initialize()).rejects.toThrow() From 2f71294ca84c4a4b58eff735dddec6004e15df81 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:43:00 +1000 Subject: [PATCH 3/6] fix(test): Increase tolerance for slow CI machines --- packages/core/src/testing/mock-keys.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/testing/mock-keys.test.ts b/packages/core/src/testing/mock-keys.test.ts index 7293bec91..0cecb6373 100644 --- a/packages/core/src/testing/mock-keys.test.ts +++ b/packages/core/src/testing/mock-keys.test.ts @@ -184,7 +184,7 @@ describe("mock-keys", () => { expect(timestamps).toHaveLength(2) expect(timestamps[1] - timestamps[0]).toBeGreaterThanOrEqual(8) // Allow some tolerance - expect(timestamps[1] - timestamps[0]).toBeLessThan(20) + expect(timestamps[1] - timestamps[0]).toBeLessThan(50) // Increased tolerance for CI/slower machines }) test("pressKey with shift modifier", () => { From 8a57df122d3ba995a3234923e80c1736a4146ac1 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:43:36 +1000 Subject: [PATCH 4/6] Update packages/core/src/lib/tree-sitter/cache.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/core/src/lib/tree-sitter/cache.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/lib/tree-sitter/cache.test.ts b/packages/core/src/lib/tree-sitter/cache.test.ts index 9a41a7b97..e602cb061 100644 --- a/packages/core/src/lib/tree-sitter/cache.test.ts +++ b/packages/core/src/lib/tree-sitter/cache.test.ts @@ -212,6 +212,8 @@ describe("TreeSitterClient Caching", () => { }) test("should handle directory creation errors gracefully", async () => { + // Use a null byte in the path to ensure it is invalid on all platforms. + // This helps test error handling for directory creation in a cross-platform way. const invalidDataPath = "/invalid\x00/path/with/null/byte" const client = new TreeSitterClient({ dataPath: invalidDataPath }) From 853a9a4cbcddc25c8e769515492a9c62b295e07b Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:19:12 +1000 Subject: [PATCH 5/6] chore: remove unneeded --- packages/core/src/zig/renderer.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/zig/renderer.zig b/packages/core/src/zig/renderer.zig index 84b45f8f8..b1daa8fed 100644 --- a/packages/core/src/zig/renderer.zig +++ b/packages/core/src/zig/renderer.zig @@ -5,7 +5,6 @@ const buf = @import("buffer.zig"); const gp = @import("grapheme.zig"); const Terminal = @import("terminal.zig"); const logger = @import("logger.zig"); -const builtin = @import("builtin"); pub const RGBA = ansi.RGBA; pub const OptimizedBuffer = buf.OptimizedBuffer; From 380fbea09c466187ccf96e533751fe2263ea25ec Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:29:08 +1000 Subject: [PATCH 6/6] fix: Try a slight delay to allow drag events to finish on a slow runner --- packages/core/src/renderables/Text.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/renderables/Text.test.ts b/packages/core/src/renderables/Text.test.ts index 7a66adb8b..de216adc3 100644 --- a/packages/core/src/renderables/Text.test.ts +++ b/packages/core/src/renderables/Text.test.ts @@ -32,6 +32,8 @@ describe("TextRenderable Selection", () => { }) await currentMouse.drag(text.x, text.y, text.x + 5, text.y) + // Add delay to ensure all drag events are processed on slow CI machines + await new Promise((resolve) => setTimeout(resolve, 50)) await renderOnce() const selectedText = text.getSelectedText() @@ -46,6 +48,8 @@ describe("TextRenderable Selection", () => { // Select "Hello 🌍" (7 characters: H,e,l,l,o, ,🌍) await currentMouse.drag(text.x, text.y, text.x + 7, text.y) + // Add delay to ensure all drag events are processed on slow CI machines + await new Promise((resolve) => setTimeout(resolve, 50)) await renderOnce() const selectedText = text.getSelectedText()