Skip to content

Commit c8b9438

Browse files
committed
Test suite: forward the app's native CodePush logs into the test log
1 parent 87fe9e8 commit c8b9438

1 file changed

Lines changed: 73 additions & 0 deletions

File tree

test/test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use strict";
22

33
import assert = require("assert");
4+
import childProcess = require("child_process");
45
import fs = require("fs");
56
import mkdirp = require("mkdirp");
67
import os = require("os");
@@ -676,6 +677,78 @@ const UpdateSync = "updateSync.js";
676677
const UpdateSync2x = "updateSync2x.js";
677678
const UpdateNotifyApplicationReadyConditional = "updateNARConditional.js";
678679

680+
//////////////////////////////////////////////////////////////////////////////////////////
681+
// Forward the app's native log into this run's output. By default a run log contains nothing
682+
// whatsoever from the native module, and there's no way to tell from a CI log which native
683+
// path a test took.
684+
685+
let nativeLogProcess: childProcess.ChildProcess = null;
686+
let nativeLogBuffer = "";
687+
688+
function readIOSLogEvent(line: string): string {
689+
try {
690+
// `log stream` opens with a human-readable banner before the ndjson starts, so
691+
// anything unparseable is expected and simply skipped. CPLog also prefixes its
692+
// format string with a newline.
693+
return (JSON.parse(line).eventMessage || "").trim() || null;
694+
} catch (error) {
695+
return null;
696+
}
697+
}
698+
699+
function startNativeLogForwarding(): void {
700+
const isIOS = TestUtil.readMochaCommandLineFlag("--ios");
701+
const isAndroid = TestUtil.readMochaCommandLineFlag("--android");
702+
if (nativeLogProcess || (!isIOS && !isAndroid)) {
703+
return;
704+
}
705+
706+
// The two platforms filter differently. iOS filters on CodePush's own message prefix in
707+
// the log command itself, and emits one JSON object per line. Android filters by log tag:
708+
// "ReactNative" is shared with React Native's own logging, but a release build barely uses
709+
// it, so it's good enough. "-T 1" starts at the tail rather than replaying earlier runs'
710+
// output, without mutating device state the way "logcat -c" would.
711+
const commandArgs = isIOS
712+
? ["simctl", "spawn", "booted", "log", "stream", "--style", "ndjson", "--predicate", "eventMessage CONTAINS \"[CodePush]\""]
713+
: ["logcat", "-v", "brief", "-T", "1", "ReactNative:D", "*:S"];
714+
715+
const logProcess = childProcess.spawn(isIOS ? "xcrun" : "adb", commandArgs, { stdio: ["ignore", "pipe", "ignore"] });
716+
717+
logProcess.stdout.setEncoding("utf8");
718+
logProcess.stdout.on("data", (chunk: string) => {
719+
nativeLogBuffer += chunk;
720+
const lines = nativeLogBuffer.split("\n");
721+
// The last element is either empty or a partial line still being written.
722+
nativeLogBuffer = lines.pop() || "";
723+
724+
lines.forEach((line) => {
725+
const message = isIOS ? readIOSLogEvent(line) : line.trim();
726+
if (message) {
727+
console.log(`[NATIVE] ${message}`);
728+
}
729+
});
730+
});
731+
732+
// Failing to stream logs must never fail the run, this is diagnostics only. The handler
733+
// is also required: an unhandled "error" event on a child process throws.
734+
logProcess.on("error", (error: Error) => {
735+
console.log(`[NATIVE] Could not stream native logs: ${error.message}`);
736+
});
737+
738+
nativeLogProcess = logProcess;
739+
}
740+
741+
beforeEach(function () {
742+
startNativeLogForwarding();
743+
});
744+
745+
after(function () {
746+
if (nativeLogProcess) {
747+
nativeLogProcess.kill();
748+
nativeLogProcess = null;
749+
}
750+
});
751+
679752
//////////////////////////////////////////////////////////////////////////////////////////
680753
// Collect iOS Simulator crash reports for failed tests (e.g. the app under test crashed and
681754
// the harness just idled until the test timeout), so they can be uploaded as CI artifacts.

0 commit comments

Comments
 (0)