Skip to content

Commit 205a966

Browse files
authored
Fix CI (#3)
* Improved directory upload handling * Increase waitUntil timeOut * test utilities: introduce waiting for modified timestamp sync * add concurrently as a dependency so that dependencies can be cached properly on CI (lockfile can't be changed)
1 parent 2c28ba7 commit 205a966

File tree

5 files changed

+107
-38
lines changed

5 files changed

+107
-38
lines changed

.github/workflows/ci.yml

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ on:
44
push:
55
branches:
66
- master
7+
pull_request:
8+
branches:
9+
- master
710

811
jobs:
912
build:
@@ -24,9 +27,7 @@ jobs:
2427
node-version: 22
2528
cache: pnpm
2629
- name: Install dependencies
27-
run: |
28-
pnpm install
29-
pnpm add concurrently
30+
run: pnpm install
3031
- name: Test
3132
run: pnpm concurrently "pnpm test" "pnpm typecheck" "pnpm lint"
3233
env:

client/tests/e2e.spec.mts

+24-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { fileExists } from "@s3-smart-sync/shared/fileExists.js";
2-
import { mkdir, readFile, rm, stat } from "node:fs/promises";
2+
import { readFile, rm, stat } from "node:fs/promises";
33
import { join } from "node:path";
44
import {
55
UNIGNORE_DURATION,
@@ -10,6 +10,7 @@ import {
1010
cleanupS3,
1111
clientLogs,
1212
createClientDirectories,
13+
createDirectory,
1314
createFile,
1415
list,
1516
pause,
@@ -32,14 +33,27 @@ globalThis.it = (name: string, fn: () => Promise<void>, timeout?: number) => {
3233
originalIt(
3334
name,
3435
async function () {
35-
process.stdout.write(`===========================================\n`);
36-
process.stdout.write(`${name}\n`);
37-
process.stdout.write(`===========================================\n`);
36+
process.stdout.write(
37+
`===============================================================================\n`,
38+
);
39+
process.stdout.write(` 🧪 ${name}\n`);
40+
process.stdout.write(
41+
`===============================================================================\n`,
42+
);
3843
return await fn();
3944
},
4045
timeout,
4146
);
4247
};
48+
// @ts-expect-error
49+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
50+
globalThis.it.only = (
51+
name: string,
52+
fn: () => Promise<void>,
53+
timeout?: number,
54+
) => {
55+
originalIt.only(name, fn, timeout);
56+
};
4357

4458
describe("E2E Tests", () => {
4559
beforeAll(async () => {
@@ -118,7 +132,9 @@ describe("E2E Tests", () => {
118132
await stopClients([1]);
119133
await Promise.all(
120134
Object.entries(TEST_FILES).map(([key, content]) =>
121-
createFile(0, key, content),
135+
key.endsWith("/")
136+
? createDirectory(0, key as `${string}/`)
137+
: createFile(0, key, content),
122138
),
123139
);
124140
await startClients([1]);
@@ -177,11 +193,7 @@ describe("E2E Tests", () => {
177193
await pause(WATCHER_DEBOUNCE_DURATION + 300);
178194
await sendSnsMessage("file-then-directory", "delete");
179195

180-
await mkdir(join(clientDirectories[0]!, "file-then-directory"));
181-
// First, the debounced upload. Then we have to wait for the upload to actually have finished
182-
await pause(WATCHER_DEBOUNCE_DURATION + 300);
183-
await sendSnsMessage("file-then-directory/", "put");
184-
196+
await createDirectory(0, "file-then-directory/");
185197
await waitUntil(async () =>
186198
(
187199
await stat(join(clientDirectories[1]!, "file-then-directory"))
@@ -190,10 +202,7 @@ describe("E2E Tests", () => {
190202
});
191203

192204
it("should handle replacing an empty directory with a file", async () => {
193-
await mkdir(join(clientDirectories[0]!, "directory-then-file"));
194-
// First, the debounced upload. Then we have to wait for the upload to actually have finished
195-
await pause(WATCHER_DEBOUNCE_DURATION + 300);
196-
await sendSnsMessage("directory-then-file/", "put");
205+
await createDirectory(0, "directory-then-file/");
197206
await waitUntil(async () =>
198207
(
199208
await stat(join(clientDirectories[1]!, "directory-then-file"))
@@ -203,7 +212,7 @@ describe("E2E Tests", () => {
203212
await rm(join(clientDirectories[0]!, "directory-then-file"), {
204213
recursive: true,
205214
});
206-
await pause(WATCHER_DEBOUNCE_DURATION + 300);
215+
await pause(WATCHER_DEBOUNCE_DURATION + 1000);
207216
await waitUntil(async () => {
208217
const { Contents } = await list("directory-then-file/");
209218
return Contents === undefined;

client/tests/utilities.ts

+33-20
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { Upload } from "@aws-sdk/lib-storage";
1010
import { logger } from "@s3-smart-sync/shared/logger.js";
1111
import { spawn } from "child_process";
12-
import { mkdir, rm, writeFile, readdir } from "node:fs/promises";
12+
import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
1313
import path, { join } from "node:path";
1414
import {
1515
ACCESS_KEY,
@@ -81,6 +81,13 @@ export async function createClientDirectories<T extends readonly number[]>(
8181
) as Record<T[number], string>;
8282
}
8383

84+
/**
85+
* Includes sending SNS message
86+
*/
87+
export async function createDirectory(id: number, key: `${string}/`) {
88+
await createFile(id, key, "");
89+
}
90+
8491
/**
8592
* Includes sending SNS message
8693
*/
@@ -93,27 +100,33 @@ export async function createFile(id: number, key: string, content: string) {
93100
await writeFile(join(clientDirectory, key), content);
94101
}
95102

96-
// We have to check content in case the file already existed
103+
let lastModified: Date | undefined;
97104
await waitUntil(async () => {
98-
if (key.endsWith("/")) {
99-
await s3Client.send(
100-
new GetObjectCommand({
101-
Bucket: S3_BUCKET,
102-
Key: key,
103-
}),
104-
);
105-
} else {
106-
const { Body } = await s3Client.send(
107-
new GetObjectCommand({
108-
Bucket: S3_BUCKET,
109-
Key: key,
110-
}),
111-
);
112-
const actualContent = await Body?.transformToString();
113-
return actualContent === content;
114-
}
105+
const { Body, LastModified } = await s3Client.send(
106+
new GetObjectCommand({
107+
Bucket: S3_BUCKET,
108+
Key: key,
109+
}),
110+
);
111+
lastModified = LastModified;
112+
113+
// We have to check content in case the file already existed
114+
const actualContent = await Body?.transformToString();
115+
return actualContent === content;
115116
});
116117

118+
// Wait for modified timestamp syncing
119+
if (lastModified) {
120+
await waitUntil(async () => {
121+
return (
122+
(await stat(join(clientDirectory, key))).mtime.valueOf() ===
123+
lastModified!.valueOf()
124+
);
125+
});
126+
} else {
127+
throw new Error("No last modified info for " + key);
128+
}
129+
117130
await sendSnsMessage(key, "put");
118131
}
119132

@@ -272,7 +285,7 @@ export async function waitUntil(
272285
fn: () => unknown,
273286
{
274287
interval = 200,
275-
timeout = 3000,
288+
timeout = 5000,
276289
}: { interval?: number; timeout?: number } = {},
277290
) {
278291
const startTime = Date.now();

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@eslint/js": "^9.21.0",
2727
"@types/eslint__js": "^8.42.3",
2828
"@types/node": "^20.17.19",
29+
"concurrently": "^9.1.2",
2930
"eslint": "^9.21.0",
3031
"eslint-config-prettier": "^9.1.0",
3132
"eslint-plugin-prettier": "^5.2.3",

pnpm-lock.yaml

+45
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)