diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3a7b95..1396715 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,8 +151,8 @@ jobs: - name: Run unit tests (Bun) if: matrix.runtime.kind == 'bun' - run: bun x vitest --no-watch + run: bun x vitest --no-watch --no-file-parallelism --sequence.concurrent=false - name: Run unit tests (Deno) if: matrix.runtime.kind == 'deno' - run: deno run -A --node-modules-dir=auto npm:vitest --no-watch + run: deno run -A --node-modules-dir=auto npm:vitest --no-watch --no-file-parallelism --sequence.concurrent=false diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 191f032..d9b1f65 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -3,19 +3,33 @@ { "name": "linux-gcc-x64", "includePath": [ - "${workspaceFolder}/**", + "${workspaceFolder}", + "${workspaceFolder}/src/cpp", + "${workspaceFolder}/node_modules/node-addon-api", + "/usr/include/node", "/usr/include/gstreamer-1.0", + "/usr/include/orc-0.4", "/usr/include/x86_64-linux-gnu", "/usr/include/glib-2.0", - "/usr/lib/x86_64-linux-gnu/glib-2.0/include", - "${workspaceFolder}/node_modules/node-addon-api" + "/usr/lib/x86_64-linux-gnu/glib-2.0/include" ], - "compilerPath": "/usr/lib/ccache/gcc", - "cStandard": "${default}", - "cppStandard": "${default}", + "browse": { + "path": [ + "${workspaceFolder}/src/cpp", + "${workspaceFolder}/node_modules/node-addon-api", + "/usr/include/node", + "/usr/include/gstreamer-1.0", + "/usr/include/orc-0.4", + "/usr/include/x86_64-linux-gnu", + "/usr/include/glib-2.0", + "/usr/lib/x86_64-linux-gnu/glib-2.0/include" + ] + }, + "compilerPath": "/usr/bin/g++", + "cStandard": "c17", + "cppStandard": "c++20", "intelliSenseMode": "linux-gcc-x64", - "compilerArgs": [""], - "defines": ["NAPI_CPP_EXCEPTIONS", "BUILDING_NODE_EXTENSION"] + "defines": ["NAPI_VERSION=9", "NAPI_DISABLE_CPP_EXCEPTIONS", "BUILDING_NODE_EXTENSION"] } ], "version": 4 diff --git a/README.md b/README.md index a010f6a..1605928 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This project represents a complete modernization of the old [node-gstreamer-supe - **Rolldown bundling**: Generates both CommonJS and ESM modules for maximum compatibility - **TypeScript-first**: Complete TypeScript support with full type definitions - **GYP build system**: Robust C++ compilation with proper dependency management -- **Modern testing**: Uses Vitest for fast, concurrent testing instead of legacy test frameworks +- **Modern testing**: Uses Vitest for fast testing instead of legacy test frameworks ### Enhanced Developer Experience @@ -1046,7 +1046,7 @@ if (sampleResult?.type === "sample") { ### Testing & Quality -- **Vitest**: Modern, fast test runner with concurrent execution +- **Vitest**: Modern, fast test runner - **Oxlint**: Fast Rust-based linter with TypeScript support - **Prettier**: Code formatting - **Coverage**: Built-in test coverage reporting @@ -1093,7 +1093,6 @@ gst-kit/ ## Performance Considerations -- **Concurrent Testing**: All tests run concurrently for faster execution - **Efficient Memory Management**: Proper buffer lifecycle management - **Async Operations**: Non-blocking operations for better performance - **Type Safety**: Compile-time error detection reduces runtime overhead diff --git a/package.json b/package.json index cfc09da..e40de26 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ "build:native": "node-gyp rebuild", "build:ts": "rolldown -c rolldown.config.mjs", "build": "npm run clean && npm run build:native && npm run build:ts", - "test:unit": "vitest --no-watch", - "test:unit:bun": "bun x vitest --no-watch", - "test:unit:deno": "deno run -A --node-modules-dir=auto npm:vitest --no-watch", + "test:unit": "vitest --no-watch --no-file-parallelism --sequence.concurrent=false", + "test:unit:bun": "bun x vitest --no-watch --no-file-parallelism --sequence.concurrent=false", + "test:unit:deno": "deno run -A --node-modules-dir=auto npm:vitest --no-watch --no-file-parallelism --sequence.concurrent=false", "test": "npm run test:unit", "prepublishOnly": "npm run build:ts", "postinstall": "node scripts/ensure-native-addon.mjs", diff --git a/src/cpp/pipeline.cpp b/src/cpp/pipeline.cpp index 486bdf6..86a3791 100644 --- a/src/cpp/pipeline.cpp +++ b/src/cpp/pipeline.cpp @@ -295,8 +295,7 @@ Napi::Value Pipeline::end_of_stream(const Napi::CallbackInfo &info) { } // Send EOS event to the pipeline - gboolean result = - gst_element_send_event(GST_ELEMENT(pipeline.get()), gst_event_new_eos()); + gboolean result = gst_element_send_event(GST_ELEMENT(pipeline.get()), gst_event_new_eos()); return Napi::Boolean::New(env, result); } diff --git a/src/ts/appsink.test.ts b/src/ts/appsink.test.ts index 639e752..1ca8f88 100644 --- a/src/ts/appsink.test.ts +++ b/src/ts/appsink.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { Pipeline, type GStreamerSample } from "."; import { arePluginsAvailable } from "./test-utils"; -describe.concurrent("AppSink", () => { +describe("AppSink", () => { it("should pull frames", async () => { const pipeline = new Pipeline("videotestsrc ! videoconvert ! appsink name=sink"); const sink = pipeline.getElementByName("sink"); @@ -13,7 +13,7 @@ describe.concurrent("AppSink", () => { const result = await sink.getSample(); - pipeline.stop(); + await pipeline.stop(); expect(result).not.toBeNull(); expect(result?.buffer).toBeDefined(); @@ -34,7 +34,7 @@ describe.concurrent("AppSink", () => { const result = await sink.getSample(10); - pipeline.stop(); + await pipeline.stop(); expect(result).toBeNull(); } @@ -48,7 +48,7 @@ describe.concurrent("AppSink", () => { const result = await sink.getSample(); - pipeline.stop(); + await pipeline.stop(); expect(result).toBeNull(); }); @@ -74,7 +74,7 @@ describe.concurrent("AppSink", () => { currFrames++; } - pipeline.stop(); + await pipeline.stop(); expect(currFrames).toBe(frames); }); @@ -102,7 +102,7 @@ describe.concurrent("AppSink", () => { }); unsubscribe(); - pipeline.stop(); + await pipeline.stop(); expect(samples).toHaveLength(frames); expect(samples[0].buffer).toBeDefined(); diff --git a/src/ts/appsrc-eos.test.ts b/src/ts/appsrc-eos.test.ts index 34ff4de..b29547a 100644 --- a/src/ts/appsrc-eos.test.ts +++ b/src/ts/appsrc-eos.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { Pipeline, type GstMessage } from "."; -describe.concurrent("AppSrc End-of-Stream", () => { +describe("AppSrc End-of-Stream", () => { it("should send EOS signal through endOfStream method", async () => { const pipeline = new Pipeline("appsrc name=source ! fakesink"); const source = pipeline.getElementByName("source"); diff --git a/src/ts/appsrc.test.ts b/src/ts/appsrc.test.ts index e41c25a..343f96f 100644 --- a/src/ts/appsrc.test.ts +++ b/src/ts/appsrc.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { Pipeline } from "."; -describe.concurrent("AppSrc", () => { +describe("AppSrc", () => { it("should push buffer to app source", () => { const pipeline = new Pipeline("appsrc name=source ! fakesink"); const source = pipeline.getElementByName("source"); diff --git a/src/ts/bus-pop.test.ts b/src/ts/bus-pop.test.ts index 8b68b25..667728a 100644 --- a/src/ts/bus-pop.test.ts +++ b/src/ts/bus-pop.test.ts @@ -2,14 +2,14 @@ import { describe, expect, it } from "vitest"; import { Pipeline, type GstMessage } from "."; import { isWindows } from "./test-utils"; -describe.concurrent("Pipeline busPop Method", () => { +describe("Pipeline busPop Method", () => { it("should return null when no message available with timeout", async () => { const pipeline = new Pipeline("videotestsrc ! fakesink"); // Try to pop a message with a very short timeout (10ms) const message = await pipeline.busPop(10); - pipeline.stop(); + await pipeline.stop(); // Should return null when timeout expires with no messages expect(message).toBeNull(); @@ -24,7 +24,7 @@ describe.concurrent("Pipeline busPop Method", () => { // Pop a message with reasonable timeout const message = await pipeline.busPop(1000); - pipeline.stop(); + await pipeline.stop(); expect(message).not.toBeNull(); expect(typeof message?.type).toBe("string"); @@ -52,7 +52,7 @@ describe.concurrent("Pipeline busPop Method", () => { } } - pipeline.stop(); + await pipeline.stop(); // Should have found at least one message during pipeline startup expect(foundMessage).toBe(true); @@ -66,7 +66,7 @@ describe.concurrent("Pipeline busPop Method", () => { const message = await pipeline.busPop(1000); - pipeline.stop(); + await pipeline.stop(); if (message && message.type === "error") { expect(message.errorMessage).toBeDefined(); @@ -84,7 +84,7 @@ describe.concurrent("Pipeline busPop Method", () => { const endTime = Date.now(); const elapsed = endTime - startTime; - pipeline.stop(); + await pipeline.stop(); // Should timeout within a reasonable range (timing varies by platform) expect(elapsed).toBeGreaterThan(80); @@ -101,7 +101,7 @@ describe.concurrent("Pipeline busPop Method", () => { // Use -1 for infinite timeout, but pipeline should generate EOS quickly const message = await pipeline.busPop(-1); - pipeline.stop(); + await pipeline.stop(); // Should get a message (likely EOS or state-changed) expect(message).not.toBeNull(); @@ -127,7 +127,7 @@ describe.concurrent("Pipeline busPop Method", () => { } } - pipeline.stop(); + await pipeline.stop(); if (messageWithStructure) { expect(messageWithStructure.structureName).toBeDefined(); diff --git a/src/ts/codec.test.ts b/src/ts/codec.test.ts index 1409d52..f1fc568 100644 --- a/src/ts/codec.test.ts +++ b/src/ts/codec.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { Pipeline, type GStreamerSample, GstBufferFlags, type BufferData } from "."; import { arePluginsAvailable } from "./test-utils"; -describe.concurrent("codec", () => { +describe("codec", () => { it.skipIf(!arePluginsAvailable(["x264enc", "rtph264pay", "rtph264depay", "h264parse"]))( "should be able to distinguish between delta and key frames", async () => { @@ -26,7 +26,7 @@ describe.concurrent("codec", () => { currFrames++; } - pipeline.stop(); + await pipeline.stop(); const keyFrame = samples.find(e => e.flags ? !(e.flags & GstBufferFlags.GST_BUFFER_FLAG_DELTA_UNIT) : false @@ -60,7 +60,7 @@ describe.concurrent("codec", () => { }); unsubscribe(); - pipeline.stop(); + await pipeline.stop(); expect(bufferData).toBeDefined(); expect(bufferData.buffer).toBeDefined(); diff --git a/src/ts/element-props.test.ts b/src/ts/element-props.test.ts index feaf345..3f7a9b8 100644 --- a/src/ts/element-props.test.ts +++ b/src/ts/element-props.test.ts @@ -1,13 +1,11 @@ import { describe, it, expect } from "vitest"; import { Pipeline } from "./"; -describe.concurrent("Element Properties", () => { +describe("Element Properties", () => { it("should get prop value", async () => { const caps = "video/x-raw,format=(string)GRAY8"; const pipeline = new Pipeline(`videotestsrc ! capsfilter name=target caps=${caps} ! fakesink`); - await pipeline.play(); - const element = pipeline.getElementByName("target"); if (!element) throw new Error("Element not found"); @@ -17,8 +15,6 @@ describe.concurrent("Element Properties", () => { expect(prop).not.toBeNull(); expect(prop?.type).toBe("primitive"); caps.split(",").forEach(cap => expect(prop?.value).toContain(cap)); - - await pipeline.stop(); }); it("should set string property", () => { diff --git a/src/ts/fakesink.test.ts b/src/ts/fakesink.test.ts index 1e902b5..8b9f2e0 100644 --- a/src/ts/fakesink.test.ts +++ b/src/ts/fakesink.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { Pipeline, type GStreamerSample } from "."; -describe.concurrent("FakeSink", () => { +describe("FakeSink", () => { it("should capture last sample when enabled", async () => { const pipeline = new Pipeline("videotestsrc ! fakesink enable-last-sample=true name=sink"); const fakesink = pipeline.getElementByName("sink"); @@ -13,7 +13,7 @@ describe.concurrent("FakeSink", () => { // Get the last sample const sampleResult = fakesink.getElementProperty("last-sample"); - pipeline.stop(); + await pipeline.stop(); expect(sampleResult).not.toBeNull(); expect(sampleResult?.type).toBe("sample"); @@ -45,7 +45,7 @@ describe.concurrent("FakeSink", () => { // Try to get the last sample (should be null since it's disabled) const sampleResult = fakesink.getElementProperty("last-sample"); - pipeline.stop(); + await pipeline.stop(); expect(sampleResult).toBeNull(); }); @@ -82,7 +82,7 @@ describe.concurrent("FakeSink", () => { const sampleResult = fakesink.getElementProperty("last-sample"); - pipeline.stop(); + await pipeline.stop(); expect(sampleResult).not.toBeNull(); expect(sampleResult?.type).toBe("sample"); @@ -108,7 +108,7 @@ describe.concurrent("FakeSink", () => { const sampleResult = fakesink.getElementProperty("last-sample"); - pipeline.stop(); + await pipeline.stop(); expect(sampleResult).not.toBeNull(); expect(sampleResult?.type).toBe("sample"); diff --git a/src/ts/pipeline-eos.test.ts b/src/ts/pipeline-eos.test.ts index 787bf06..a344f4a 100644 --- a/src/ts/pipeline-eos.test.ts +++ b/src/ts/pipeline-eos.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { Pipeline, type GstMessage } from "."; -describe.concurrent("Pipeline EOS - State-Gated Dispatch", () => { +describe("Pipeline EOS - State-Gated Dispatch", () => { it("should return true when pipeline is in PLAYING state", async () => { const pipeline = new Pipeline("videotestsrc ! fakesink"); @@ -10,6 +10,8 @@ describe.concurrent("Pipeline EOS - State-Gated Dispatch", () => { const result = pipeline.endOfStream(); expect(result).toBe(true); + await new Promise(resolve => setTimeout(resolve, 30)); + await pipeline.stop(); }); @@ -21,6 +23,8 @@ describe.concurrent("Pipeline EOS - State-Gated Dispatch", () => { const result = pipeline.endOfStream(); expect(result).toBe(true); + await new Promise(resolve => setTimeout(resolve, 30)); + await pipeline.stop(); }); @@ -30,6 +34,8 @@ describe.concurrent("Pipeline EOS - State-Gated Dispatch", () => { const result = pipeline.endOfStream(); expect(result).toBe(false); + await new Promise(resolve => setTimeout(resolve, 30)); + await pipeline.stop(); }); @@ -53,6 +59,8 @@ describe.concurrent("Pipeline EOS - State-Gated Dispatch", () => { } } + await new Promise(resolve => setTimeout(resolve, 30)); + await pipeline.stop(); expect(eosMessage).not.toBeNull(); @@ -72,6 +80,8 @@ describe.concurrent("Pipeline EOS - State-Gated Dispatch", () => { expect(result2).toBe(true); expect(result3).toBe(true); + await new Promise(resolve => setTimeout(resolve, 30)); + await pipeline.stop(); }); @@ -83,6 +93,8 @@ describe.concurrent("Pipeline EOS - State-Gated Dispatch", () => { const resultWhilePlaying = pipeline.endOfStream(); expect(resultWhilePlaying).toBe(true); + await new Promise(resolve => setTimeout(resolve, 30)); + await pipeline.stop(); const resultAfterStop = pipeline.endOfStream(); diff --git a/src/ts/pipeline-pad.test.ts b/src/ts/pipeline-pad.test.ts index 95411a2..814fc2e 100644 --- a/src/ts/pipeline-pad.test.ts +++ b/src/ts/pipeline-pad.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import { Pipeline } from "./"; -describe.concurrent("Pipeline Pad Methods", () => { +describe("Pipeline Pad Methods", () => { it("should get pad information from an element", () => { const pipeline = new Pipeline("videotestsrc name=source ! fakesink name=sink"); const element = pipeline.getElementByName("source"); @@ -57,6 +57,8 @@ describe.concurrent("Pipeline Pad Methods", () => { expect(() => sel.setPad("active-pad", "sink_0")).not.toThrow(); expect(() => sel.setPad("active-pad", "sink_1")).not.toThrow(); expect(() => sel.setPad("active-pad", "sink_0")).not.toThrow(); + + await pipeline.stop(); }); it("should throw error for setPad with invalid element", () => { diff --git a/src/ts/pipeline-query.test.ts b/src/ts/pipeline-query.test.ts index 22542f5..8fb1c03 100644 --- a/src/ts/pipeline-query.test.ts +++ b/src/ts/pipeline-query.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { Pipeline } from "."; -describe.concurrent("Pipeline Query Methods", () => { +describe("Pipeline Query Methods", () => { it("should query position from videotestsrc pipeline", async () => { const pipeline = new Pipeline("videotestsrc num-buffers=100 ! fakesink"); @@ -9,7 +9,7 @@ describe.concurrent("Pipeline Query Methods", () => { const position = pipeline.queryPosition(); - pipeline.stop(); + await pipeline.stop(); // Position should be a number (could be -1 if not available) expect(typeof position).toBe("number"); @@ -22,7 +22,7 @@ describe.concurrent("Pipeline Query Methods", () => { const duration = pipeline.queryDuration(); - pipeline.stop(); + await pipeline.stop(); // Duration should be a number (could be -1 if not available) expect(typeof duration).toBe("number"); @@ -54,7 +54,7 @@ describe.concurrent("Pipeline Query Methods", () => { const position2 = pipeline.queryPosition(); - pipeline.stop(); + await pipeline.stop(); expect(typeof position1).toBe("number"); expect(typeof position2).toBe("number"); diff --git a/src/ts/pipeline-seek.test.ts b/src/ts/pipeline-seek.test.ts index ab9e181..d198eff 100644 --- a/src/ts/pipeline-seek.test.ts +++ b/src/ts/pipeline-seek.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { Pipeline } from "."; import { isWindows } from "./test-utils"; -describe.concurrent("Pipeline Seek Method", () => { +describe("Pipeline Seek Method", () => { it("should seek to a specific position in a video pipeline", async () => { const pipeline = new Pipeline( "videotestsrc num-buffers=300 ! video/x-raw,framerate=30/1 ! fakesink" diff --git a/src/ts/pipeline-state.test.ts b/src/ts/pipeline-state.test.ts index 9a54a4a..41fa102 100644 --- a/src/ts/pipeline-state.test.ts +++ b/src/ts/pipeline-state.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { Pipeline } from "."; -describe.concurrent("Pipeline State Management", () => { +describe("Pipeline State Management", () => { it("should play a pipeline", async () => { const pipeline = new Pipeline("videotestsrc ! fakesink"); @@ -127,6 +127,8 @@ describe.concurrent("Pipeline State Management", () => { expect(positionWhilePlaying).toBeGreaterThan(0); expect(positionWhilePaused).toBeGreaterThanOrEqual(0); expect(positionAfterResume).toBeGreaterThan(0); + + await pipeline.stop(); }); it("should provide detailed state change information", async () => { diff --git a/src/ts/rtp-stats.test.ts b/src/ts/rtp-stats.test.ts index 013c544..a4dc1e8 100644 --- a/src/ts/rtp-stats.test.ts +++ b/src/ts/rtp-stats.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { Pipeline, type BufferData } from "."; import { arePluginsAvailable } from "./test-utils"; -describe.concurrent("RTP Statistics", () => { +describe("RTP Statistics", () => { it.skipIf(!arePluginsAvailable(["x264enc", "rtph264pay", "rtph264depay", "h264parse"]))( "should extract RTP stats from rtph264depay stats property", async () => { @@ -18,7 +18,7 @@ describe.concurrent("RTP Statistics", () => { const statsResult = depayloader.getElementProperty("stats"); - pipeline.stop(); + await pipeline.stop(); expect(statsResult).toBeDefined(); expect(statsResult).not.toBeNull(); @@ -76,7 +76,7 @@ describe.concurrent("RTP Statistics", () => { const statsResult = depayloader.getElementProperty("stats"); - pipeline.stop(); + await pipeline.stop(); // Verify pad probe captured RTP data expect(padProbeRtpData).toBeDefined(); @@ -101,7 +101,7 @@ describe.concurrent("RTP Statistics", () => { const statsResult = sink.getElementProperty("stats"); - pipeline.stop(); + await pipeline.stop(); // fakesink has stats but different type (basesink stats, not RTP depayload stats) expect(statsResult).toBeDefined(); diff --git a/vitest.config.ts b/vitest.config.ts index 34590cc..d9c07f4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ reporter: ["text", "json", "html"], exclude: ["node_modules/", "dist/"], }, - slowTestThreshold: 2000, + slowTestThreshold: 5000, testTimeout: 20000, env: { NODE_ENV: "test",