From 5f6967e84300a810fd4d745034128a535085ec23 Mon Sep 17 00:00:00 2001 From: kobenguyent <7845001+kobenguyent@users.noreply.github.com> Date: Tue, 19 Aug 2025 08:49:03 +0000 Subject: [PATCH 1/2] fix share workers --- .../sandbox/workers-proxy-issue/README.md | 48 +++++++++++++++ .../workers-proxy-issue/codecept.conf.js | 10 ++++ .../sandbox/workers-proxy-issue/final_test.js | 60 +++++++++++++++++++ .../sandbox/workers-proxy-issue/proxy_test.js | 42 +++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 test/data/sandbox/workers-proxy-issue/README.md create mode 100644 test/data/sandbox/workers-proxy-issue/codecept.conf.js create mode 100644 test/data/sandbox/workers-proxy-issue/final_test.js create mode 100644 test/data/sandbox/workers-proxy-issue/proxy_test.js diff --git a/test/data/sandbox/workers-proxy-issue/README.md b/test/data/sandbox/workers-proxy-issue/README.md new file mode 100644 index 000000000..e876b60d8 --- /dev/null +++ b/test/data/sandbox/workers-proxy-issue/README.md @@ -0,0 +1,48 @@ +# Test Suite for Issue #5066 Fix + +This directory contains tests that validate the fix for **Issue #5066: Unable to inject data between workers because of proxy object**. + +## Test Files + +### `proxy_test.js` +Basic tests that verify the core functionality of `share()` and `inject()` functions: +- Basic data sharing with primitive types (strings, numbers) +- Complex nested data structures (objects, arrays) +- Property access patterns that should work after the fix + +### `final_test.js` +Comprehensive end-to-end validation test that covers: +- Multiple data types and structures +- Data overriding scenarios +- Deep nested property access +- Key enumeration functionality +- Real-world usage patterns + +## Running the Tests + +### Single-threaded execution: +```bash +npx codeceptjs run proxy_test.js +npx codeceptjs run final_test.js +``` + +### Multi-worker execution (tests worker communication): +```bash +npx codeceptjs run-workers 2 proxy_test.js +npx codeceptjs run-workers 2 final_test.js +``` + +## What the Fix Addresses + +1. **Circular Dependency Error**: Fixed "Support object undefined is not defined" error in `workerStorage.js` +2. **Proxy System Enhancement**: Updated container proxy system to handle dynamically shared data +3. **Worker Communication**: Ensured data sharing works correctly between worker threads +4. **Key Enumeration**: Made sure `Object.keys(inject())` shows shared properties + +## Expected Results + +All tests should pass in both single-threaded and multi-worker modes, demonstrating that: +- `share({ data })` correctly shares data between workers +- `inject()` returns a proxy object with proper access to shared data +- Both direct property access and nested object traversal work correctly +- Key enumeration shows all shared properties diff --git a/test/data/sandbox/workers-proxy-issue/codecept.conf.js b/test/data/sandbox/workers-proxy-issue/codecept.conf.js new file mode 100644 index 000000000..cc2767e51 --- /dev/null +++ b/test/data/sandbox/workers-proxy-issue/codecept.conf.js @@ -0,0 +1,10 @@ +exports.config = { + tests: './proxy_test.js', + output: './output', + helpers: { + FileSystem: {} + }, + include: {}, + mocha: {}, + name: 'workers-proxy-issue', +}; diff --git a/test/data/sandbox/workers-proxy-issue/final_test.js b/test/data/sandbox/workers-proxy-issue/final_test.js new file mode 100644 index 000000000..dd280654c --- /dev/null +++ b/test/data/sandbox/workers-proxy-issue/final_test.js @@ -0,0 +1,60 @@ +const assert = require('assert'); + +Feature('Complete validation for issue #5066 fix'); + +Scenario('End-to-end worker data sharing validation', () => { + console.log('=== Testing complete data sharing workflow ==='); + + // Test 1: Basic data sharing + share({ + message: 'Hello from main thread', + config: { timeout: 5000, retries: 3 }, + users: ['alice', 'bob', 'charlie'] + }); + + const data = inject(); + + // Verify all property types work correctly + assert.strictEqual(data.message, 'Hello from main thread', 'String property should work'); + assert.strictEqual(data.config.timeout, 5000, 'Nested object property should work'); + assert.strictEqual(data.config.retries, 3, 'Nested object property should work'); + assert(Array.isArray(data.users), 'Array property should work'); + assert.strictEqual(data.users.length, 3, 'Array length should work'); + assert.strictEqual(data.users[0], 'alice', 'Array access should work'); + + // Test 2: Data overriding + share({ message: 'Updated message' }); + const updatedData = inject(); + assert.strictEqual(updatedData.message, 'Updated message', 'Data override should work'); + assert.strictEqual(updatedData.config.timeout, 5000, 'Previous data should persist'); + + // Test 3: Complex nested structures + share({ + testSuite: { + name: 'E2E Tests', + tests: [ + { name: 'Login test', status: 'passed', data: { user: 'admin', pass: 'secret' } }, + { name: 'Checkout test', status: 'failed', error: 'Timeout occurred' } + ], + metadata: { + browser: 'chrome', + version: '91.0', + viewport: { width: 1920, height: 1080 } + } + } + }); + + const complexData = inject(); + assert.strictEqual(complexData.testSuite.name, 'E2E Tests', 'Deep nested string should work'); + assert.strictEqual(complexData.testSuite.tests[0].data.user, 'admin', 'Very deep nested access should work'); + assert.strictEqual(complexData.testSuite.metadata.viewport.width, 1920, 'Very deep nested number should work'); + + // Test 4: Key enumeration + const allKeys = Object.keys(inject()); + assert(allKeys.includes('message'), 'Keys should include shared properties'); + assert(allKeys.includes('testSuite'), 'Keys should include all shared properties'); + + console.log('✅ ALL TESTS PASSED - Issue #5066 is completely fixed!'); + console.log('✅ Workers can now share and inject data without circular dependency errors'); + console.log('✅ Proxy objects work correctly for both direct and nested property access'); +}); diff --git a/test/data/sandbox/workers-proxy-issue/proxy_test.js b/test/data/sandbox/workers-proxy-issue/proxy_test.js new file mode 100644 index 000000000..519e65498 --- /dev/null +++ b/test/data/sandbox/workers-proxy-issue/proxy_test.js @@ -0,0 +1,42 @@ +const assert = require('assert'); + +Feature('Fix for issue #5066: Unable to inject data between workers because of proxy object'); + +Scenario('Basic share and inject functionality', () => { + console.log('Testing basic share() and inject() functionality...'); + + // This is the basic pattern that should work after the fix + const originalData = { message: 'Hello', count: 42 }; + share(originalData); + + const injectedData = inject(); + console.log('Shared data keys:', Object.keys(originalData)); + console.log('Injected data keys:', Object.keys(injectedData)); + + // These assertions should pass after the fix + assert.strictEqual(injectedData.message, 'Hello', 'String property should be accessible'); + assert.strictEqual(injectedData.count, 42, 'Number property should be accessible'); + + console.log('✅ SUCCESS: Basic share/inject works!'); +}); + +Scenario('Complex nested data structures', () => { + console.log('Testing complex nested data sharing...'); + + const testDataJson = { + users: [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }], + settings: { theme: 'dark', language: 'en' } + }; + + share({ testDataJson }); + + const data = inject(); + + // These should work after the fix + assert(data.testDataJson, 'testDataJson should be accessible'); + assert(Array.isArray(data.testDataJson.users), 'users should be an array'); + assert.strictEqual(data.testDataJson.users[0].name, 'John', 'Should access nested user data'); + assert.strictEqual(data.testDataJson.settings.theme, 'dark', 'Should access nested settings'); + + console.log('✅ SUCCESS: Complex nested data works!'); +}); From 2c3de954a7f2b41c2324fd3c1d0aa75c4f14883c Mon Sep 17 00:00:00 2001 From: kobenguyent <7845001+kobenguyent@users.noreply.github.com> Date: Tue, 19 Aug 2025 08:49:14 +0000 Subject: [PATCH 2/2] fix share workers --- docs/parallel.md | 71 ++++++++++++++++++++++++++------- lib/container.js | 19 +++++++-- lib/workerStorage.js | 3 +- test/unit/workerStorage_test.js | 35 ++++++++++++++++ 4 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 test/unit/workerStorage_test.js diff --git a/docs/parallel.md b/docs/parallel.md index 913eb2d6f..bea099046 100644 --- a/docs/parallel.md +++ b/docs/parallel.md @@ -364,37 +364,78 @@ workers.on(event.all.result, (status, completedTests, workerStats) => { ## Sharing Data Between Workers -NodeJS Workers can communicate between each other via messaging system. It may happen that you want to pass some data from one of the workers to other. For instance, you may want to share user credentials accross all tests. Data will be appended to a container. +NodeJS Workers can communicate between each other via messaging system. CodeceptJS allows you to share data between different worker processes using the `share()` and `inject()` functions. -However, you can't access uninitialized data from a container, so to start, you need to initialize data first. Inside `bootstrap` function of the config we execute the `share` to initialize value: +### Basic Usage +You can share data directly using the `share()` function and access it using `inject()`: + +```js +// In one test or worker +share({ userData: { name: 'user', password: '123456' } }); + +// In another test or worker +const testData = inject(); +console.log(testData.userData.name); // 'user' +console.log(testData.userData.password); // '123456' +``` + +### Initializing Data in Bootstrap + +For complex scenarios where you need to initialize shared data before tests run, you can use the bootstrap function: ```js // inside codecept.conf.js exports.config = { bootstrap() { - // append empty userData to container - share({ userData: false }); + // Initialize shared data container + share({ userData: null, config: { retries: 3 } }); } } ``` -Now each worker has `userData` inside a container. However, it is empty. -When you obtain real data in one of the tests you can now `share` this data accross tests. Use `inject` function to access data inside a container: +Then in your tests, you can check and update the shared data: ```js -// get current value of userData -let { userData } = inject(); -// if userData is still empty - update it -if (!userData) { - userData = { name: 'user', password: '123456' }; - // now new userData will be shared accross all workers - share({userData : userData}); +const testData = inject(); +if (!testData.userData) { + // Update shared data - both approaches work: + share({ userData: { name: 'user', password: '123456' } }); + // or mutate the injected object: + testData.userData = { name: 'user', password: '123456' }; } ``` -If you want to share data only within same worker, and not across all workers, you need to add option `local: true` every time you run `share` +### Working with Proxy Objects + +Since CodeceptJS 3.7.0+, shared data uses Proxy objects for synchronization between workers. The proxy system works seamlessly for most use cases: + +```js +// ✅ All of these work correctly: +const data = inject(); +console.log(data.userData.name); // Access nested properties +console.log(Object.keys(data)); // Enumerate shared keys +data.newProperty = 'value'; // Add new properties +Object.assign(data, { more: 'data' }); // Merge objects +``` + +**Important Note:** Avoid reassigning the entire injected object: + +```js +// ❌ AVOID: This breaks the proxy reference +let testData = inject(); +testData = someOtherObject; // This will NOT work as expected! + +// ✅ PREFERRED: Use share() to replace data or mutate properties +share({ userData: someOtherObject }); // This works! +// or +Object.assign(inject(), someOtherObject); // This works! +``` + +### Local Data (Worker-Specific) + +If you want to share data only within the same worker (not across all workers), use the `local` option: ```js -share({ userData: false }, {local: true }); +share({ localData: 'worker-specific' }, { local: true }); ``` diff --git a/lib/container.js b/lib/container.js index 68b26ecce..13a4337c4 100644 --- a/lib/container.js +++ b/lib/container.js @@ -28,6 +28,7 @@ let container = { translation: {}, /** @type {Result | null} */ result: null, + sharedKeys: new Set() // Track keys shared via share() function } /** @@ -174,6 +175,7 @@ class Container { container.translation = loadTranslation() container.proxySupport = createSupportObjects(newSupport) container.plugins = newPlugins + container.sharedKeys = new Set() // Clear shared keys asyncHelperPromise = Promise.resolve() store.actor = null debug('container cleared') @@ -197,7 +199,13 @@ class Container { * @param {Object} options - set {local: true} to not share among workers */ static share(data, options = {}) { - Container.append({ support: data }) + // Instead of using append which replaces the entire container, + // directly update the support object to maintain proxy references + Object.assign(container.support, data) + + // Track which keys were explicitly shared + Object.keys(data).forEach(key => container.sharedKeys.add(key)) + if (!options.local) { WorkerStorage.share(data) } @@ -396,10 +404,11 @@ function createSupportObjects(config) { {}, { has(target, key) { - return keys.includes(key) + return keys.includes(key) || container.sharedKeys.has(key) }, ownKeys() { - return keys + // Return both original config keys and explicitly shared keys + return [...new Set([...keys, ...container.sharedKeys])] }, getOwnPropertyDescriptor(target, prop) { return { @@ -409,6 +418,10 @@ function createSupportObjects(config) { } }, get(target, key) { + // First check if this is an explicitly shared property + if (container.sharedKeys.has(key) && key in container.support) { + return container.support[key] + } return lazyLoad(key) }, }, diff --git a/lib/workerStorage.js b/lib/workerStorage.js index 8c5fdbf5e..2e7a5c6c5 100644 --- a/lib/workerStorage.js +++ b/lib/workerStorage.js @@ -7,7 +7,8 @@ const invokeWorkerListeners = (workerObj) => { const { threadId } = workerObj; workerObj.on('message', (messageData) => { if (messageData.event === shareEvent) { - share(messageData.data); + const Container = require('./container'); + Container.share(messageData.data); } }); workerObj.on('exit', () => { diff --git a/test/unit/workerStorage_test.js b/test/unit/workerStorage_test.js new file mode 100644 index 000000000..8a1f95750 --- /dev/null +++ b/test/unit/workerStorage_test.js @@ -0,0 +1,35 @@ +const { expect } = require('expect'); +const WorkerStorage = require('../../lib/workerStorage'); +const { Worker } = require('worker_threads'); +const event = require('../../lib/event'); + +describe('WorkerStorage', () => { + it('should handle share message correctly without circular dependency', (done) => { + // Create a mock worker to test the functionality + const mockWorker = { + threadId: 'test-thread-1', + on: (eventName, callback) => { + if (eventName === 'message') { + // Simulate receiving a share message + setTimeout(() => { + callback({ event: 'share', data: { testKey: 'testValue' } }); + done(); + }, 10); + } + }, + postMessage: () => {} + }; + + // Add the mock worker to storage + WorkerStorage.addWorker(mockWorker); + }); + + it('should not crash when sharing data', () => { + const testData = { user: 'test', password: '123' }; + + // This should not throw an error + expect(() => { + WorkerStorage.share(testData); + }).not.toThrow(); + }); +});