Skip to content

Commit c3d29e2

Browse files
committed
update tests
1 parent 86a28ef commit c3d29e2

File tree

9 files changed

+116
-115
lines changed

9 files changed

+116
-115
lines changed

airbyte-local-cli-nodejs/package-lock.json

Lines changed: 1 addition & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

airbyte-local-cli-nodejs/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@
5454
"dockerode": "^4.0.2",
5555
"lodash": "^4.17.21",
5656
"pino": "^9.6.0",
57-
"pino-pretty": "^13.0.0",
58-
"yoctocolors-cjs": "^2.1.2"
57+
"pino-pretty": "^13.0.0"
5958
},
6059
"jest": {
6160
"silent": false,

airbyte-local-cli-nodejs/src/docker.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import {writeFileSync} from 'node:fs';
1+
import {createWriteStream, writeFileSync} from 'node:fs';
22
import {Writable} from 'node:stream';
33

44
import Docker from 'dockerode';
55

66
import {AirbyteConnectionStatus, AirbyteConnectionStatusMessage, AirbyteMessageType, FarosConfig} from './types';
7-
import {DEFAULT_STATE_FILE, logger, SRC_CATALOG_FILENAME, SRC_CONFIG_FILENAME} from './utils';
7+
import {
8+
DEFAULT_STATE_FILE,
9+
logger,
10+
OutputStream,
11+
processSrcDataByLine,
12+
SRC_CATALOG_FILENAME,
13+
SRC_CONFIG_FILENAME,
14+
SRC_OUTPUT_DATA_FILE,
15+
} from './utils';
816

917
// Constants
1018
const DEFAULT_MAX_LOG_SIZE = '10m';
@@ -119,10 +127,8 @@ export async function checkSrcConnection(tmpDir: string, image: string, srcConfi
119127
* --config "/configs/$src_config_filename" \
120128
* --catalog "/configs/$src_catalog_filename" \
121129
* --state "/configs/$src_state_filename"
122-
*
123-
* @argument command - for testing purposes only
124130
*/
125-
export async function runSrcSync(tmpDir: string, config: FarosConfig): Promise<string> {
131+
export async function runSrcSync(tmpDir: string, config: FarosConfig): Promise<void> {
126132
logger.info('Running source connector...');
127133

128134
if (!config.src?.image) {
@@ -180,35 +186,41 @@ export async function runSrcSync(tmpDir: string, config: FarosConfig): Promise<s
180186
const cidfilePath = `tmp-${timestamp}-src_cid`;
181187
writeFileSync(cidfilePath, container.id);
182188

189+
// Create a writable stream for the processed output data
190+
const srcOutputFilePath = config.srcOutputFile ?? `${tmpDir}/${SRC_OUTPUT_DATA_FILE}`;
191+
const srcOutputStream =
192+
config.srcOutputFile === OutputStream.STDOUT ? process.stdout : createWriteStream(srcOutputFilePath);
193+
183194
// create a writable stream to capture the stdout
184-
// TODO: write to a file instead of memory
185-
let data = '';
186-
const stdoutStream = new Writable({
195+
let buffer = '';
196+
const containerOutputStream = new Writable({
187197
write(chunk, _encoding, callback) {
188-
data += chunk.toString();
198+
buffer += chunk.toString();
199+
const lines = buffer.split('\n');
200+
buffer = lines.pop() ?? '';
201+
lines.forEach((line: string) => {
202+
processSrcDataByLine(line, srcOutputStream, config);
203+
});
189204
callback();
190205
},
191206
});
207+
192208
// Attach the stderr to termincal stderr, and stdout to the output stream
193209
const stream = await container.attach({stream: true, stdout: true, stderr: true});
194-
container.modem.demuxStream(stream, stdoutStream, process.stderr);
210+
container.modem.demuxStream(stream, containerOutputStream, process.stderr);
195211

196212
// Start the container
197213
await container.start();
198214

199215
// Wait for the container to finish
200216
const res = await container.wait();
201217
logger.debug(res);
202-
logger.debug(data);
203218

204219
if (res.StatusCode === 0) {
205220
logger.info('Source connector ran successfully.');
206221
} else {
207222
throw new Error('Failed to run source connector.');
208223
}
209-
210-
// return the stdout data
211-
return data;
212224
} catch (error: any) {
213225
throw new Error(`Failed to run source connector: ${error.message ?? JSON.stringify(error)}`);
214226
}

airbyte-local-cli-nodejs/src/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {parseAndValidateInputs} from './command';
22
import {checkDockerInstalled, checkSrcConnection, pullDockerImage, runSrcSync} from './docker';
33
import {AirbyteCliContext} from './types';
4-
import {cleanUp, createTmpDir, loadStateFile, logger, processSrcData, writeConfig} from './utils';
4+
import {cleanUp, createTmpDir, loadStateFile, logger, processSrcInputFile, writeConfig} from './utils';
55

66
async function main(): Promise<void> {
77
const context: AirbyteCliContext = {};
@@ -28,10 +28,9 @@ async function main(): Promise<void> {
2828
// Run airbyte source connector
2929
if (!cfg.srcInputFile) {
3030
await runSrcSync(context.tmpDir, cfg);
31+
} else {
32+
await processSrcInputFile(context.tmpDir, cfg);
3133
}
32-
33-
// Process source data
34-
await processSrcData(cfg);
3534
} catch (error: any) {
3635
logger.error(error.message, 'Error');
3736
cleanUp(context);

airbyte-local-cli-nodejs/src/utils.ts

Lines changed: 47 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import {
1111
} from 'node:fs';
1212
import {tmpdir} from 'node:os';
1313
import {sep} from 'node:path';
14+
import readline from 'node:readline';
15+
import {Writable} from 'node:stream';
1416

1517
import pino from 'pino';
1618
import pretty from 'pino-pretty';
17-
import readline from 'readline';
1819

1920
import {AirbyteCliContext, AirbyteConfig, FarosConfig} from './types';
2021

@@ -32,11 +33,6 @@ export const DEFAULT_STATE_FILE = 'state.json';
3233
export const SRC_INPUT_DATA_FILE = `${FILENAME_PREFIX}_src_data`;
3334
export const SRC_OUTPUT_DATA_FILE = `${FILENAME_PREFIX}_src_output`;
3435

35-
// Check if the value is an OutputStream
36-
function isOutputStream(value: any): value is OutputStream {
37-
return Object.values(OutputStream).includes(value);
38-
}
39-
4036
// Create a pino logger instance
4137
export const logger = pino(pretty({colorize: true}));
4238

@@ -209,8 +205,6 @@ export function writeFile(file: string, data: any): void {
209205
/**
210206
* Process the source output.
211207
*
212-
* jq_src_msg="\"${GREEN}[SRC]: \" + ${JQ_TIMESTAMP} + \" - \" + ."
213-
*
214208
* Command line:
215209
* tee >(
216210
* jq -cR $jq_color_opt --unbuffered 'fromjson? |
@@ -220,62 +214,53 @@ export function writeFile(file: string, data: any): void {
220214
* jq -cR --unbuffered "fromjson? |
221215
* select(.type == \"RECORD\" or .type == \"STATE\") |
222216
* .record.stream |= \"${dst_stream_prefix}\" + ." |
223-
* tee "$output_filepath" |
217+
* tee "$output_filepath" | ...
218+
*
219+
* jq_src_msg="\"${GREEN}[SRC]: \" + ${JQ_TIMESTAMP} + \" - \" + ."
220+
*
224221
*
225222
* Note: `dst_stream_prefix` command option is dropped
226223
*/
227-
export function processSrcData(cfg: FarosConfig): Promise<void> {
228-
return new Promise((resolve, reject) => {
229-
// Reformat the JSON message
230-
function formatSrcMsg(json: any): string {
231-
return `[SRC] - ${JSON.stringify(json)}`;
232-
}
233224

234-
// Processing the source line by line
235-
function processLine(line: string): void {
236-
// skip empty lines
237-
if (line.trim() === '') {
238-
return;
239-
}
240-
241-
try {
242-
const data = JSON.parse(line);
225+
// Processing the source line by line
226+
export function processSrcDataByLine(line: string, outputStream: Writable, cfg: FarosConfig): void {
227+
// Reformat the JSON message
228+
function formatSrcMsg(json: any): string {
229+
return `[SRC] - ${JSON.stringify(json)}`;
230+
}
231+
// skip empty lines
232+
if (line.trim() === '') {
233+
return;
234+
}
243235

244-
// non RECORD and STATE type messages: print as stdout
245-
if (data.type !== 'RECORD' && data.type !== 'STATE') {
246-
logger.info(formatSrcMsg(data));
247-
}
248-
// RECORD and STATE type messages: logger or write to output file
249-
else {
250-
if (cfg.srcOutputFile === OutputStream.STDOUT) {
251-
logger.info(formatSrcMsg(data));
252-
} else {
253-
outputStream.write(`${line}\n`);
254-
}
255-
}
256-
} catch (error: any) {
257-
rl.emit('error', new Error(`Line of data: '${line}'; Error: ${error.message}`));
236+
try {
237+
const data = JSON.parse(line);
238+
239+
// non RECORD and STATE type messages: print as stdout
240+
// RECORD and STATE type messages: when the output is set to stdout
241+
if ((data.type !== 'RECORD' && data.type !== 'STATE') || cfg.srcOutputFile === OutputStream.STDOUT) {
242+
if (cfg.rawMessages) {
243+
process.stdout.write(`${line}\n`);
244+
} else {
245+
logger.info(formatSrcMsg(data));
258246
}
259247
}
260-
261-
// Close the output stream if it's a file
262-
function closeOutputStream(): void {
263-
if (!isOutputStream(cfg.srcOutputFile)) {
264-
outputStream.end();
265-
}
266-
logger.debug(`Closing the output stream file '${cfg.srcOutputFile}'.`);
248+
// RECORD and STATE type messages: write to output file
249+
else {
250+
outputStream.write(`${line}\n`);
267251
}
252+
} catch (error: any) {
253+
throw new Error(`Line of data: '${line}'; Error: ${error.message}`);
254+
}
255+
}
268256

269-
// get source data file and output file paths
270-
const srcInputFilePath = cfg.srcInputFile ?? SRC_INPUT_DATA_FILE;
271-
const srcOutputFilePath = cfg.srcOutputFile ?? SRC_OUTPUT_DATA_FILE;
272-
257+
export function processSrcInputFile(tmpDir: string, cfg: FarosConfig): Promise<void> {
258+
return new Promise((resolve, reject) => {
273259
// create input and output streams:
274-
// - input stream: read from the data file user provided or the one the script created in the temporary directory
275-
// - output stream: write to a file or stdout. Overwrite the file if it exists, otherwise create a new one
276-
const inputStream = createReadStream(srcInputFilePath);
277-
const outputStream: any =
278-
cfg.srcOutputFile === OutputStream.STDOUT ? process.stdout : createWriteStream(srcOutputFilePath);
260+
// - input stream: read from the data file user provided
261+
// - output stream: write to an intermediate file. Overwrite the file if it exists, otherwise create a new one
262+
const inputStream = createReadStream(cfg.srcInputFile!);
263+
const outputStream = createWriteStream(`${tmpDir}/${SRC_OUTPUT_DATA_FILE}`);
279264

280265
// create readline interface
281266
const rl = readline.createInterface({
@@ -284,20 +269,24 @@ export function processSrcData(cfg: FarosConfig): Promise<void> {
284269
});
285270

286271
rl.on('line', (line) => {
287-
processLine(line);
272+
try {
273+
processSrcDataByLine(line, outputStream, cfg);
274+
} catch (error: any) {
275+
rl.emit('error', error);
276+
}
288277
})
289278
.on('close', () => {
290279
logger.debug('Finished processing the source output data.');
291-
closeOutputStream();
280+
outputStream.end();
292281
resolve();
293282
})
294283
.on('error', (error) => {
295-
closeOutputStream();
284+
outputStream.end();
296285
reject(new Error(`Failed to process the source output data: ${error.message ?? JSON.stringify(error)}`));
297286
});
298287

299288
outputStream.on('error', (error: any) => {
300-
closeOutputStream();
289+
outputStream.end();
301290
reject(new Error(`Failed to write to the output file: ${error.message ?? JSON.stringify(error)}`));
302291
});
303292
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`runSrcSync should success 1`] = `
4+
"{"state":{"data":{"format":"base64/gzip","data":"H4sIAAAAAAAAA6uuBQBDv6ajAgAAAA=="}},"type":"STATE","redactedConfig":{"user":"chris"},"sourceType":"example","sourceVersion":"0.12.4"}
5+
{"state":{"data":{"format":"base64/gzip","data":"H4sIAAAAAAAAA6uuBQBDv6ajAgAAAA=="}},"type":"STATE","sourceStatus":{"status":"SUCCESS"}}
6+
{"state":{"data":{"format":"base64/gzip","data":"H4sIAAAAAAAAA6uuBQBDv6ajAgAAAA=="}},"type":"STATE","logs":[{"timestamp":***,"message":{"level":30,"msg":"Source version: 0.12.4"}},{"timestamp":***,"message":{"level":30,"msg":"Config: {\\"user\\":\\"chris\\"}"}},{"timestamp":***,"message":{"level":30,"msg":"Catalog: {}"}},{"timestamp":***,"message":{"level":30,"msg":"State: {}"}},{"timestamp":***,"message":{"level":30,"msg":"Syncing ExampleSource"}},{"timestamp":***,"message":{"level":30,"msg":"Finished syncing ExampleSource"}}]}
7+
"
8+
`;

airbyte-local-cli-nodejs/test/__snapshots__/utils.it.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ exports[`parseConfigFile should pass 1`] = `
3434
}
3535
`;
3636

37-
exports[`processSrcData should succeed writing to an output file 1`] = `
37+
exports[`processSrcInputFile should succeed writing to an output file 1`] = `
3838
"{"state":{"data":{"format":"base64/gzip","data":"H4sIAAAAAAAAA6uuBQBDv6ajAgAAAA=="}},"type":"STATE","redactedConfig":{"user":"chris"},"sourceType":"example","sourceVersion":"0.12.3"}
3939
{"state":{"data":{"format":"base64/gzip","data":"H4sIAAAAAAAAA6uuBQBDv6ajAgAAAA=="}},"type":"STATE","sourceStatus":{"status":"SUCCESS"}}
4040
{"state":{"data":{"format":"base64/gzip","data":"H4sIAAAAAAAAA6uuBQBDv6ajAgAAAA=="}},"type":"STATE","logs":[{"timestamp":1736891682696,"message":{"level":30,"msg":"Source version: 0.12.3"}},{"timestamp":1736891682697,"message":{"level":30,"msg":"Config: {\\"user\\":\\"chris\\"}"}},{"timestamp":1736891682697,"message":{"level":30,"msg":"Catalog: {}"}},{"timestamp":1736891682697,"message":{"level":30,"msg":"State: {}"}},{"timestamp":1736891682700,"message":{"level":30,"msg":"Syncing ExampleSource"}},{"timestamp":1736891682704,"message":{"level":30,"msg":"Finished syncing ExampleSource"}}]}

airbyte-local-cli-nodejs/test/docker.it.test.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import {readdirSync, unlinkSync} from 'node:fs';
1+
import {readdirSync, readFileSync, rmSync, unlinkSync} from 'node:fs';
22
import path from 'node:path';
33
import {Writable} from 'node:stream';
44

55
import {checkSrcConnection, pullDockerImage, runSrcSync} from '../src/docker';
66
import {FarosConfig} from '../src/types';
7+
import {SRC_OUTPUT_DATA_FILE} from '../src/utils';
78

89
const defaultConfig: FarosConfig = {
910
srcCheckConnection: false,
@@ -48,14 +49,21 @@ describe('checkSrcConnection', () => {
4849
});
4950
});
5051

51-
describe('runSrcSync', () => {
52+
describe.only('runSrcSync', () => {
5253
const testCfg: FarosConfig = {
5354
...defaultConfig,
5455
src: {
5556
image: 'farosai/airbyte-example-source',
5657
},
5758
};
5859

60+
const testTmpDir = `${process.cwd()}/test/resources/dockerIt_runSrcSync`;
61+
62+
// remove the intermediate output file
63+
afterEach(() => {
64+
rmSync(`${testTmpDir}/${SRC_OUTPUT_DATA_FILE}`, {force: true});
65+
});
66+
5967
// Clean up files created by the test
6068
afterAll(() => {
6169
const pattern = /.*-src_cid$/;
@@ -69,10 +77,15 @@ describe('runSrcSync', () => {
6977
});
7078

7179
it('should success', async () => {
72-
await expect(runSrcSync(`${process.cwd()}/test/resources/dockerIt_runSrcSync`, testCfg)).resolves.not.toThrow();
80+
await expect(runSrcSync(testTmpDir, testCfg)).resolves.not.toThrow();
81+
82+
// Replace timestamp for comparison
83+
const output = readFileSync(`${testTmpDir}/${SRC_OUTPUT_DATA_FILE}`, 'utf8');
84+
const outputWithoutTS = output.split('\n').map((line) => line.replace(/"timestamp":\d+/g, '"timestamp":***'));
85+
expect(outputWithoutTS.join('\n')).toMatchSnapshot();
7386
});
7487

75-
// Check the error message is correctly redirect to process.stderr
88+
// Check stderr message is correctly redirect to process.stderr
7689
it('should fail', async () => {
7790
// Capture process.stderr
7891
let stderrData = '';
@@ -87,7 +100,7 @@ describe('runSrcSync', () => {
87100

88101
try {
89102
await expect(
90-
runSrcSync(`${process.cwd()}/test/resources/dockerIt_runSrcSync`, {
103+
runSrcSync(testTmpDir, {
91104
...testCfg,
92105
src: {image: 'farosai/airbyte-faros-graphql-source'},
93106
}),

0 commit comments

Comments
 (0)