Skip to content

Commit da2deca

Browse files
authored
Merge pull request #475 from roy232828/improve-cypress-ci-speed
Improve cypress ci speed
2 parents 207a96b + 2642f3a commit da2deca

File tree

18 files changed

+209
-171
lines changed

18 files changed

+209
-171
lines changed

.github/workflows/test.yml

Lines changed: 30 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
name: Test CI
22

3-
on: [push, pull_request]
3+
on: [ push, pull_request ]
44

55
jobs:
66
build-artifact:
7+
name: Build And Test
78
runs-on: ubuntu-latest
89
steps:
910
- uses: actions/checkout@v4
@@ -29,6 +30,14 @@ jobs:
2930
restore-keys: |
3031
${{ runner.os }}-modules-
3132
33+
- name: Restore Turbo cache
34+
uses: actions/cache/restore@v4
35+
id: restore-turbo-cache
36+
with:
37+
path: .turbo/cache
38+
key: ${{ runner.os }}-turbo-${{ hashFiles('**/yarn.lock') }}
39+
restore-keys: |
40+
${{ runner.os }}-turbo-
3241
- name: Restore Cypress cache
3342
uses: actions/cache/restore@v4
3443
id: restore-cypress-cache
@@ -47,10 +56,17 @@ jobs:
4756
- name: cypress install
4857
if: |
4958
steps.restore-cypress-cache.outputs.cache-hit != 'true'
50-
run: yarn cypress install
51-
52-
- name: yarn build
53-
run: yarn build
59+
run: |
60+
# run yarn cypress verify, if it fails, run yarn cypress install \
61+
if ! yarn cypress verify; then \
62+
yarn cypress install \
63+
fi
64+
65+
- name: Build and Test
66+
run: |
67+
yarn turbo run build && \
68+
# skipping testing ds-ext for now \
69+
yarn turbo run test --filter=!ds-ext
5470
5571
- name: Upload build dist artifacts
5672
uses: actions/upload-artifact@v4
@@ -78,7 +94,15 @@ jobs:
7894
!**/node_modules/.cache
7995
!**/node_modules/**/node_modules
8096
key: ${{ steps.restore-node-modules.outputs.cache-primary-key }}
81-
97+
98+
# Save Turbo cache (always execute)
99+
- name: Save Turbo cache
100+
if: success()
101+
uses: actions/cache/save@v4
102+
with:
103+
path: .turbo/cache
104+
key: ${{ steps.restore-turbo-cache.outputs.cache-primary-key }}
105+
82106
# Save Cypress cache (always execute)
83107
- name: Save Cypress cache
84108
if: success()
@@ -90,95 +114,3 @@ jobs:
90114
- name: Cleanup
91115
if: always()
92116
run: yarn cache clean
93-
94-
unit-test:
95-
needs: build-artifact
96-
runs-on: ubuntu-latest
97-
steps:
98-
- uses: actions/checkout@v4
99-
100-
- name: Use Node.js
101-
uses: actions/setup-node@v4
102-
with:
103-
node-version: '21.x'
104-
registry-url: 'https://registry.npmjs.org'
105-
106-
- name: Restore node_modules
107-
uses: actions/cache/restore@v4
108-
id: restore-node-modules
109-
with:
110-
path: |
111-
node_modules
112-
packages/**/node_modules
113-
!node_modules/@data-story
114-
!packages/*/node_modules/@data-story
115-
!**/node_modules/.cache
116-
!**/node_modules/**/node_modules
117-
118-
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
119-
restore-keys: |
120-
${{ runner.os }}-modules-
121-
122-
- name: Restore Cypress cache
123-
uses: actions/cache/restore@v4
124-
id: restore-cypress-cache
125-
with:
126-
path: /home/runner/.cache/Cypress
127-
key: ${{ runner.os }}-cypress-${{ hashFiles('**/yarn.lock') }}
128-
restore-keys: |
129-
${{ runner.os }}-cypress-
130-
131-
- name: Download dist artifacts
132-
uses: actions/download-artifact@v4
133-
with:
134-
name: dist-artifacts
135-
path: packages
136-
137-
- name: Run @data-story/core, @data-story/ui, @data-story/docs tests
138-
run: yarn run ci:test-packages
139-
140-
e2e-test:
141-
needs: build-artifact
142-
runs-on: ubuntu-latest
143-
steps:
144-
- uses: actions/checkout@v4
145-
146-
- name: Use Node.js
147-
uses: actions/setup-node@v4
148-
with:
149-
node-version: '21.x'
150-
registry-url: 'https://registry.npmjs.org'
151-
152-
- name: Restore node_modules
153-
uses: actions/cache/restore@v4
154-
id: restore-node-modules
155-
with:
156-
path: |
157-
node_modules
158-
packages/**/node_modules
159-
!node_modules/@data-story
160-
!packages/*/node_modules/@data-story
161-
!**/node_modules/.cache
162-
!**/node_modules/**/node_modules
163-
164-
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
165-
restore-keys: |
166-
${{ runner.os }}-modules-
167-
168-
- name: Restore Cypress cache
169-
uses: actions/cache/restore@v4
170-
id: restore-cypress-cache
171-
with:
172-
path: /home/runner/.cache/Cypress
173-
key: ${{ runner.os }}-cypress-${{ hashFiles('**/yarn.lock') }}
174-
restore-keys: |
175-
${{ runner.os }}-cypress-
176-
177-
- name: Download build artifacts
178-
uses: actions/download-artifact@v4
179-
with:
180-
name: dist-artifacts
181-
path: packages
182-
183-
- name: Run e2e tests
184-
run: yarn run ci:e2e

cypress.config.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
import { defineConfig } from 'cypress';
2-
import webpackConfig from './packages/ui/cypress-webpack.config';
2+
import bundlerConfig from './packages/ui/cypress-vite.config';
33

44
export default defineConfig({
55
component: {
66
specPattern: '**/*.cy.{ts,tsx}',
77
devServer: {
88
framework: 'react',
9-
bundler: 'webpack',
10-
webpackConfig: webpackConfig,
9+
bundler: 'vite',
10+
viteConfig: bundlerConfig,
1111
},
1212
viewportWidth: 384,
1313
viewportHeight: 216,
1414
},
15-
16-
e2e: {
17-
baseUrl: 'http://localhost:3009',
18-
specPattern: 'cypress/e2e/**/*',
19-
setupNodeEvents(on, config) {
20-
// implement node event listeners here
21-
},
22-
},
2315
});

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,9 @@
2929
"release-ui": "yarn workspace @data-story/ui run release",
3030
"release-nodejs": "yarn workspace @data-story/nodejs run release",
3131
"release-ds-ext": "yarn workspace ds-ext run release",
32-
"ci:test": "yarn ci:test-packages && yarn ci:e2e",
32+
"ci:test": "yarn ci:test-packages",
3333
"ci:test-packages": "turbo test --filter=@data-story/core --filter=@data-story/ui --filter=@data-story/docs",
34-
"ci:e2e": "start-server-and-test 'turbo dev --parallel -- --port 3009' 3009 'cy:e2e' ",
3534
"core:test": "turbo test --filter=@data-story/core",
36-
"cy:e2e": "cypress run --e2e",
3735
"cy:open": "cypress open",
3836
"constraints": "yarn constraints"
3937
},

packages/core/src/ExecutionMemoryFactory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Hook } from './types/Hook'
1212
import { ItemValue } from './types/ItemValue'
1313
import { LinkId } from './types/Link'
1414
import { Node, NodeId } from './types/Node'
15+
import { withNodeExecutionErrorHandling } from './utils/withNodeExecutionErrorHandling'
1516

1617
export class ExecutionMemoryFactory {
1718
constructor(
@@ -64,7 +65,7 @@ export class ExecutionMemoryFactory {
6465

6566
// Initialize runner context
6667
const context = new NodeRunnerContext(node.id);
67-
context.status = computer.run({
68+
context.status = withNodeExecutionErrorHandling(computer.run.bind(computer), node)({
6869
input: inputDevice,
6970
output: outputDevice,
7071
params: this.makeParamsDevice(node, memory),

packages/core/src/ItemWithParams/ItemWithParams.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { ItemValue } from '../types/ItemValue';
2-
import { Param, EvaluatedParamValue } from '../Param';
2+
import { EvaluatedParamValue, Param } from '../Param';
33
import { ParamEvaluator } from './ParamEvaluator';
44

55
export const isItemWithParams = (item: ItemWithParams | unknown): item is ItemWithParams => {
66
// This does not always catch all cases
7-
if(item instanceof ItemWithParams) return true;
7+
if (item instanceof ItemWithParams) return true;
88

9-
if(
9+
if (
1010
item !== null
1111
&& typeof item === 'object'
1212
// @ts-ignore
@@ -16,10 +16,10 @@ export const isItemWithParams = (item: ItemWithParams | unknown): item is ItemWi
1616
) return true;
1717

1818
return false;
19-
}
19+
};
2020

2121
export class ItemWithParams<ExpectedType extends ItemValue = ItemValue> {
22-
type = 'ItemWithParams' as const
22+
type = 'ItemWithParams' as const;
2323
value: ExpectedType;
2424
params: Record<string, EvaluatedParamValue>;
2525

@@ -37,11 +37,11 @@ export class ItemWithParams<ExpectedType extends ItemValue = ItemValue> {
3737
try {
3838
const paramEvaluatorInstance = new ParamEvaluator();
3939
return paramEvaluatorInstance.evaluate(value, param, globalParams);
40-
} catch (error) {
41-
console.error('error', error);
42-
return param.input;
40+
} catch (e) {
41+
console.error('Failed while evaluating param', param, e);
42+
throw e;
4343
}
4444
},
4545
});
4646
}
47-
}
47+
}

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,4 @@ export type { Param, ParamInput as ParamValue } from './Param'
5959
export type { Port, AbstractPort } from './types/Port'
6060
export type { ReportCallback } from './types/ReportCallback';
6161
export type { ServiceProvider } from './types/ServiceProvider'
62-
export { Table } from './computers'
62+
export { Table } from './computers'

packages/core/src/types/Errors.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Node } from './Node';
2+
3+
export class NodeExecutionError extends Error {
4+
constructor(message: string, public node: Pick<Node, 'id' | 'label' | 'type'>) {
5+
super(`Error in node ${node.label || node.id} (${node.type}): ${message}`);
6+
this.name = 'NodeExecutionError';
7+
}
8+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { withNodeExecutionErrorHandling } from './withNodeExecutionErrorHandling';
3+
import { NodeExecutionError } from '../types/Errors';
4+
5+
describe('withNodeExecutionErrorHandling', () => {
6+
const mockNode = {
7+
id: 'test-node-1',
8+
label: 'Test Node',
9+
type: 'TestComputer',
10+
};
11+
12+
it('should yield values normally when no error occurs', async () => {
13+
const mockAsyncGenerator = async function* () {
14+
yield 'value1';
15+
yield 'value2';
16+
return 'final';
17+
};
18+
19+
const wrappedGenerator = withNodeExecutionErrorHandling(mockAsyncGenerator, mockNode);
20+
const generator = wrappedGenerator();
21+
22+
const result1 = await generator.next();
23+
expect(result1.value).toBe('value1');
24+
expect(result1.done).toBe(false);
25+
26+
const result2 = await generator.next();
27+
expect(result2.value).toBe('value2');
28+
expect(result2.done).toBe(false);
29+
30+
const result3 = await generator.next();
31+
expect(result3.value).toBe('final');
32+
expect(result3.done).toBe(true);
33+
});
34+
35+
it('should wrap errors with NodeExecutionError', async () => {
36+
const errorMessage = 'Something went wrong';
37+
const mockAsyncGenerator = async function* () {
38+
yield 'value1';
39+
throw new Error(errorMessage);
40+
};
41+
42+
const wrappedGenerator = withNodeExecutionErrorHandling(mockAsyncGenerator, mockNode);
43+
const generator = wrappedGenerator();
44+
45+
const result1 = await generator.next();
46+
expect(result1.value).toBe('value1');
47+
48+
await expect(generator.next()).rejects.toThrow(NodeExecutionError);
49+
50+
try {
51+
await generator.next();
52+
} catch (error) {
53+
expect(error).toBeInstanceOf(NodeExecutionError);
54+
expect((error as NodeExecutionError).node).toEqual(mockNode);
55+
expect((error as NodeExecutionError).message).toContain(errorMessage);
56+
expect((error as NodeExecutionError).message).toContain(mockNode.label);
57+
expect((error as NodeExecutionError).message).toContain(mockNode.type);
58+
}
59+
});
60+
61+
it('should handle non-Error objects thrown', async () => {
62+
const mockAsyncGenerator = async function* () {
63+
throw 'string error';
64+
};
65+
66+
const wrappedGenerator = withNodeExecutionErrorHandling(mockAsyncGenerator, mockNode);
67+
const generator = wrappedGenerator();
68+
69+
await expect(generator.next()).rejects.toThrow(NodeExecutionError);
70+
71+
try {
72+
await generator.next();
73+
} catch (error) {
74+
expect(error).toBeInstanceOf(NodeExecutionError);
75+
expect((error as NodeExecutionError).message).toContain('string error');
76+
}
77+
});
78+
79+
it('should pass arguments correctly to the wrapped function', async () => {
80+
const mockAsyncGenerator = async function* (arg1: string, arg2: number) {
81+
yield `${arg1}-${arg2}`;
82+
};
83+
84+
const wrappedGenerator = withNodeExecutionErrorHandling(mockAsyncGenerator, mockNode);
85+
const generator = wrappedGenerator('test', 42);
86+
87+
const result = await generator.next();
88+
expect(result.value).toBe('test-42');
89+
});
90+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { NodeExecutionError } from '../types/Errors';
2+
import { Node } from '../types/Node';
3+
4+
/**
5+
* High-order helper function that wraps an async generator function
6+
* and rethrows any error thrown by the run function wrapped with NodeExecutionError
7+
*
8+
* @param runFn - The async generator function to wrap
9+
* @param node - The node context for error reporting
10+
* @returns A wrapped async generator that handles errors
11+
*/
12+
export function withNodeExecutionErrorHandling<TArgs extends any[], TYield, TReturn, TNext>(
13+
runFn: (...args: TArgs) => AsyncGenerator<TYield, TReturn, TNext>,
14+
node: Pick<Node, 'id' | 'label' | 'type'>,
15+
) {
16+
return async function* (...args: TArgs): AsyncGenerator<TYield, TReturn, TNext> {
17+
try {
18+
return yield* runFn(...args);
19+
} catch (error) {
20+
const errorMessage = error instanceof Error ? error.message : String(error);
21+
throw new NodeExecutionError(errorMessage, node);
22+
}
23+
};
24+
}

0 commit comments

Comments
 (0)