@react-native-harness/platform-android 1.2.0 → 1.4.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/app-monitor.test.js +180 -102
- package/dist/__tests__/crash-diagnostics.test.d.ts +2 -0
- package/dist/__tests__/crash-diagnostics.test.d.ts.map +1 -0
- package/dist/__tests__/crash-diagnostics.test.js +119 -0
- package/dist/__tests__/crash-reporter.test.d.ts +2 -0
- package/dist/__tests__/crash-reporter.test.d.ts.map +1 -0
- package/dist/__tests__/crash-reporter.test.js +150 -0
- package/dist/__tests__/instance.test.js +133 -24
- package/dist/adb.d.ts +4 -0
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +42 -2
- package/dist/app-session.d.ts +17 -0
- package/dist/app-session.d.ts.map +1 -0
- package/dist/app-session.js +209 -0
- package/dist/crash-diagnostics.d.ts +40 -0
- package/dist/crash-diagnostics.d.ts.map +1 -0
- package/dist/crash-diagnostics.js +256 -0
- package/dist/crash-reporter.d.ts +12 -0
- package/dist/crash-reporter.d.ts.map +1 -0
- package/dist/crash-reporter.js +150 -0
- package/dist/device-state.d.ts +6 -0
- package/dist/device-state.d.ts.map +1 -0
- package/dist/device-state.js +142 -0
- package/dist/instance.d.ts +1 -1
- package/dist/instance.d.ts.map +1 -1
- package/dist/instance.js +32 -59
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/__tests__/crash-diagnostics.test.ts +144 -0
- package/src/__tests__/crash-reporter.test.ts +167 -0
- package/src/__tests__/instance.test.ts +174 -31
- package/src/adb.ts +61 -2
- package/src/app-session.ts +300 -0
- package/src/crash-diagnostics.ts +471 -0
- package/src/crash-reporter.ts +263 -0
- package/src/instance.ts +56 -91
- package/src/runner.ts +7 -3
- package/test/setup.ts +1 -0
- package/src/app-monitor.ts +0 -577
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { escapeRegExp, logger } from '@react-native-harness/tools';
|
|
2
|
+
import { androidCrashParser } from './crash-parser.js';
|
|
3
|
+
const crashDiagnosticsLogger = logger.child('android-crash-diagnostics');
|
|
4
|
+
const DROPBOX_ENTRY_HEADER = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{3})?) (\S+) \(text, \d+ bytes\)/;
|
|
5
|
+
const DROPBOX_POLL_INTERVAL_MS = 1500;
|
|
6
|
+
const DROPBOX_WAIT_TIMEOUT_MS = 10000;
|
|
7
|
+
export const DROPBOX_CRASH_TAGS = [
|
|
8
|
+
'data_app_crash',
|
|
9
|
+
'data_app_native_crash',
|
|
10
|
+
];
|
|
11
|
+
const getDropboxArtifactType = (tag) => tag === 'data_app_native_crash' ? 'dropbox-native-crash' : 'dropbox-crash';
|
|
12
|
+
const getDropboxFileName = (tag) => tag === 'data_app_native_crash'
|
|
13
|
+
? 'dropbox-native-crash.txt'
|
|
14
|
+
: 'dropbox-crash.txt';
|
|
15
|
+
const parseDropboxTimestamp = (timestamp) => {
|
|
16
|
+
const match = timestamp.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{3}))?$/);
|
|
17
|
+
if (!match) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
const [, year, month, day, hour, minute, second, millis = '0'] = match;
|
|
21
|
+
return Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second), Number(millis));
|
|
22
|
+
};
|
|
23
|
+
const extractDropboxPid = (content) => {
|
|
24
|
+
const match = content.match(/^PID:\s*(\d+)/m) ?? content.match(/\bpid:\s*(\d+)/im);
|
|
25
|
+
return match ? Number(match[1]) : undefined;
|
|
26
|
+
};
|
|
27
|
+
const extractDropboxProcessName = (content, bundleId) => {
|
|
28
|
+
const processMatch = content.match(/^Process:\s*(\S+)/m);
|
|
29
|
+
const packageMatch = content.match(/^Package:\s*(\S+)/m);
|
|
30
|
+
if (processMatch?.[1] === bundleId) {
|
|
31
|
+
return processMatch[1];
|
|
32
|
+
}
|
|
33
|
+
if (packageMatch?.[1]?.startsWith(bundleId)) {
|
|
34
|
+
return bundleId;
|
|
35
|
+
}
|
|
36
|
+
return content.includes(bundleId) ? bundleId : undefined;
|
|
37
|
+
};
|
|
38
|
+
export const parseDropboxOutput = (output) => {
|
|
39
|
+
if (output.trim() === '') {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const sections = output.split(/^={10,}\s*$/m);
|
|
43
|
+
const entries = [];
|
|
44
|
+
for (const section of sections) {
|
|
45
|
+
const trimmedSection = section.trim();
|
|
46
|
+
if (trimmedSection === '') {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const lines = trimmedSection.split('\n');
|
|
50
|
+
const headerIndex = lines.findIndex((line) => DROPBOX_ENTRY_HEADER.test(line.trim()));
|
|
51
|
+
if (headerIndex === -1) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const headerMatch = lines[headerIndex].trim().match(DROPBOX_ENTRY_HEADER);
|
|
55
|
+
if (!headerMatch) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const [, timestamp, tag] = headerMatch;
|
|
59
|
+
const content = lines.slice(headerIndex + 1).join('\n').trim();
|
|
60
|
+
if (content === '') {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
entries.push({
|
|
64
|
+
tag,
|
|
65
|
+
timestamp,
|
|
66
|
+
content,
|
|
67
|
+
pid: extractDropboxPid(content),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return entries;
|
|
71
|
+
};
|
|
72
|
+
const matchesDropboxEntry = ({ entry, bundleId, pid, }) => {
|
|
73
|
+
const processName = extractDropboxProcessName(entry.content, bundleId);
|
|
74
|
+
if (!processName) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (pid !== undefined && entry.pid !== undefined && entry.pid !== pid) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
};
|
|
82
|
+
const scoreDropboxEntry = ({ entry, bundleId, pid, occurredAt, }) => {
|
|
83
|
+
let score = 0;
|
|
84
|
+
if (extractDropboxProcessName(entry.content, bundleId) === bundleId) {
|
|
85
|
+
score += 100;
|
|
86
|
+
}
|
|
87
|
+
if (pid !== undefined && entry.pid === pid) {
|
|
88
|
+
score += 200;
|
|
89
|
+
}
|
|
90
|
+
const entryTimestamp = parseDropboxTimestamp(entry.timestamp);
|
|
91
|
+
if (entryTimestamp > 0) {
|
|
92
|
+
score -= Math.min(Math.abs(entryTimestamp - occurredAt) / 1000, 300);
|
|
93
|
+
}
|
|
94
|
+
if (entry.tag === 'data_app_native_crash') {
|
|
95
|
+
score += 25;
|
|
96
|
+
}
|
|
97
|
+
return score;
|
|
98
|
+
};
|
|
99
|
+
const toDropboxCrashArtifact = ({ entry, bundleId, pid, occurredAt, lookup, }) => {
|
|
100
|
+
const parsed = androidCrashParser.parse({
|
|
101
|
+
contents: entry.content,
|
|
102
|
+
bundleId,
|
|
103
|
+
pid: pid ?? entry.pid,
|
|
104
|
+
});
|
|
105
|
+
const artifactType = getDropboxArtifactType(entry.tag);
|
|
106
|
+
return {
|
|
107
|
+
...parsed,
|
|
108
|
+
source: 'logs',
|
|
109
|
+
summary: entry.content,
|
|
110
|
+
rawLines: entry.content.split(/\r?\n/),
|
|
111
|
+
artifactType,
|
|
112
|
+
dropboxTag: entry.tag,
|
|
113
|
+
occurredAt,
|
|
114
|
+
score: scoreDropboxEntry({
|
|
115
|
+
entry,
|
|
116
|
+
bundleId,
|
|
117
|
+
pid: lookup?.pid ?? pid,
|
|
118
|
+
occurredAt: lookup?.occurredAt ?? occurredAt,
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
const getMatchingDropboxEntries = ({ output, bundleId, pid, occurredAt, minOccurredAt, }) => parseDropboxOutput(output)
|
|
123
|
+
.filter((entry) => matchesDropboxEntry({ entry, bundleId, pid }))
|
|
124
|
+
.filter((entry) => {
|
|
125
|
+
const entryOccurredAt = parseDropboxTimestamp(entry.timestamp);
|
|
126
|
+
return (minOccurredAt === undefined ||
|
|
127
|
+
entryOccurredAt === 0 ||
|
|
128
|
+
entryOccurredAt >= minOccurredAt);
|
|
129
|
+
})
|
|
130
|
+
.map((entry) => toDropboxCrashArtifact({
|
|
131
|
+
entry,
|
|
132
|
+
bundleId,
|
|
133
|
+
pid,
|
|
134
|
+
occurredAt: parseDropboxTimestamp(entry.timestamp) || occurredAt,
|
|
135
|
+
}))
|
|
136
|
+
.sort((left, right) => {
|
|
137
|
+
if ((right.score ?? 0) !== (left.score ?? 0)) {
|
|
138
|
+
return (right.score ?? 0) - (left.score ?? 0);
|
|
139
|
+
}
|
|
140
|
+
return right.occurredAt - left.occurredAt;
|
|
141
|
+
});
|
|
142
|
+
const persistDropboxArtifact = ({ artifact, crashArtifactWriter, testFilePath, }) => {
|
|
143
|
+
if (!crashArtifactWriter) {
|
|
144
|
+
return artifact;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
...artifact,
|
|
148
|
+
artifactPath: crashArtifactWriter.persistArtifact({
|
|
149
|
+
artifactKind: artifact.artifactType,
|
|
150
|
+
testFilePath,
|
|
151
|
+
source: {
|
|
152
|
+
kind: 'text',
|
|
153
|
+
fileName: getDropboxFileName(artifact.dropboxTag),
|
|
154
|
+
text: `${artifact.summary ?? ''}\n`,
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
export const collectDropboxArtifacts = async ({ bundleId, crashArtifactWriter, getDropboxOutput, minOccurredAt, ...lookup }) => {
|
|
160
|
+
crashDiagnosticsLogger.debug('collecting dropbox crash artifacts: %o', {
|
|
161
|
+
bundleId,
|
|
162
|
+
pid: lookup.pid,
|
|
163
|
+
occurredAt: lookup.occurredAt,
|
|
164
|
+
});
|
|
165
|
+
let output = '';
|
|
166
|
+
try {
|
|
167
|
+
output = await getDropboxOutput();
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
crashDiagnosticsLogger.debug('failed to read dropbox entries', error);
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
return getMatchingDropboxEntries({
|
|
174
|
+
output,
|
|
175
|
+
bundleId,
|
|
176
|
+
pid: lookup.pid,
|
|
177
|
+
occurredAt: lookup.occurredAt,
|
|
178
|
+
minOccurredAt,
|
|
179
|
+
}).map((artifact) => persistDropboxArtifact({
|
|
180
|
+
artifact,
|
|
181
|
+
crashArtifactWriter,
|
|
182
|
+
testFilePath: lookup.testFilePath,
|
|
183
|
+
}));
|
|
184
|
+
};
|
|
185
|
+
export const waitForDropboxArtifacts = async (options) => {
|
|
186
|
+
const deadline = Date.now() + DROPBOX_WAIT_TIMEOUT_MS;
|
|
187
|
+
let latestArtifacts = [];
|
|
188
|
+
while (Date.now() < deadline) {
|
|
189
|
+
latestArtifacts = await collectDropboxArtifacts(options);
|
|
190
|
+
if (latestArtifacts.length > 0) {
|
|
191
|
+
return latestArtifacts;
|
|
192
|
+
}
|
|
193
|
+
if (Date.now() >= deadline) {
|
|
194
|
+
return latestArtifacts;
|
|
195
|
+
}
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, DROPBOX_POLL_INTERVAL_MS));
|
|
197
|
+
}
|
|
198
|
+
return latestArtifacts;
|
|
199
|
+
};
|
|
200
|
+
export const filterExitInfo = ({ output, bundleId, pid, }) => {
|
|
201
|
+
if (output.trim() === '' || /No exit info records/i.test(output)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const packagePattern = new RegExp(`\\bpackage=${escapeRegExp(bundleId)}\\b`, 'i');
|
|
205
|
+
if (!packagePattern.test(output)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const sections = output.split(/(?=ApplicationExitInfo\b)/);
|
|
209
|
+
const matchingSections = sections.filter((section) => {
|
|
210
|
+
if (!packagePattern.test(section)) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
if (pid === undefined) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return new RegExp(`\\bpid=${pid}\\b`).test(section);
|
|
217
|
+
});
|
|
218
|
+
if (matchingSections.length === 0) {
|
|
219
|
+
return packagePattern.test(output) ? output.trim() : null;
|
|
220
|
+
}
|
|
221
|
+
return matchingSections.join('\n').trim();
|
|
222
|
+
};
|
|
223
|
+
export const collectExitInfoArtifact = async ({ bundleId, crashArtifactWriter, getExitInfo, pid, testFilePath, }) => {
|
|
224
|
+
let output = '';
|
|
225
|
+
try {
|
|
226
|
+
output = await getExitInfo();
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
crashDiagnosticsLogger.debug('failed to read activity exit-info', error);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const filtered = filterExitInfo({ output, bundleId, pid });
|
|
233
|
+
if (!filtered) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
if (!crashArtifactWriter) {
|
|
237
|
+
return {
|
|
238
|
+
artifactType: 'exit-info',
|
|
239
|
+
artifactPath: filtered,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
artifactType: 'exit-info',
|
|
244
|
+
artifactPath: crashArtifactWriter.persistArtifact({
|
|
245
|
+
artifactKind: 'exit-info',
|
|
246
|
+
testFilePath,
|
|
247
|
+
source: {
|
|
248
|
+
kind: 'text',
|
|
249
|
+
fileName: 'exit-info.txt',
|
|
250
|
+
text: `${filtered}\n`,
|
|
251
|
+
},
|
|
252
|
+
}),
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
export const getBestDropboxArtifact = (artifacts) => [...artifacts].sort((left, right) => (right.score ?? 0) - (left.score ?? 0))[0] ??
|
|
256
|
+
null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AppCrashDetails, AppSessionLog, CrashArtifactWriter, CrashDetailsLookupOptions } from '@react-native-harness/platforms';
|
|
2
|
+
export declare const createAndroidCrashReporter: ({ bundleId, crashArtifactWriter, getLogs, getDropboxOutput, getExitInfo, minOccurredAt, }: {
|
|
3
|
+
bundleId: string;
|
|
4
|
+
crashArtifactWriter?: CrashArtifactWriter;
|
|
5
|
+
getLogs: () => AppSessionLog[];
|
|
6
|
+
getDropboxOutput?: () => Promise<string>;
|
|
7
|
+
getExitInfo?: () => Promise<string>;
|
|
8
|
+
minOccurredAt?: number;
|
|
9
|
+
}) => {
|
|
10
|
+
getCrashDetails: ({ occurredAt, pid, testFilePath, }: CrashDetailsLookupOptions) => Promise<AppCrashDetails | null>;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=crash-reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crash-reporter.d.ts","sourceRoot":"","sources":["../src/crash-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EAE1B,MAAM,iCAAiC,CAAC;AAsKzC,eAAO,MAAM,0BAA0B,GAAI,2FAOxC;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,OAAO,EAAE,MAAM,aAAa,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;0DAKI,yBAAyB,KAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;CAuE9D,CAAC"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { androidCrashParser } from './crash-parser.js';
|
|
2
|
+
import { collectExitInfoArtifact, waitForDropboxArtifacts, getBestDropboxArtifact, } from './crash-diagnostics.js';
|
|
3
|
+
const CRASH_ARTIFACT_SETTLE_DELAY_MS = 100;
|
|
4
|
+
const CRASH_BLOCK_HEADER = '--------- beginning of crash';
|
|
5
|
+
const CRASH_LOG_WINDOW_MS = 5000;
|
|
6
|
+
const findCrashBlockStart = (lines) => {
|
|
7
|
+
let latestCrashHeaderIndex = -1;
|
|
8
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
9
|
+
if (/FATAL EXCEPTION:|Process:\s+.+,\s+PID:|>>>\s+.+\s+<<</i.test(lines[index])) {
|
|
10
|
+
latestCrashHeaderIndex = index;
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const latestBlockHeaderIndex = lines.lastIndexOf(CRASH_BLOCK_HEADER);
|
|
15
|
+
return latestBlockHeaderIndex >= 0
|
|
16
|
+
? latestBlockHeaderIndex
|
|
17
|
+
: latestCrashHeaderIndex;
|
|
18
|
+
};
|
|
19
|
+
const getCrashBlock = (logs, occurredAt) => {
|
|
20
|
+
const nearbyLogs = logs.filter((log) => Math.abs(log.occurredAt - occurredAt) <= CRASH_LOG_WINDOW_MS);
|
|
21
|
+
const lines = nearbyLogs.length > 0
|
|
22
|
+
? nearbyLogs.map((log) => log.line)
|
|
23
|
+
: logs.map((log) => log.line);
|
|
24
|
+
const blockStart = findCrashBlockStart(lines);
|
|
25
|
+
return blockStart >= 0 ? lines.slice(blockStart) : lines;
|
|
26
|
+
};
|
|
27
|
+
const hasUsefulLogcatBlock = (rawLines) => rawLines.some((line) => /FATAL EXCEPTION:|Process:\s+.+,\s+PID:|>>>\s+.+\s+<<</i.test(line));
|
|
28
|
+
const getLogcatCrashDetails = ({ rawLines, bundleId, pid, }) => ({
|
|
29
|
+
...androidCrashParser.parse({
|
|
30
|
+
contents: rawLines.join('\n'),
|
|
31
|
+
bundleId,
|
|
32
|
+
pid,
|
|
33
|
+
}),
|
|
34
|
+
artifactType: 'logcat',
|
|
35
|
+
rawLines,
|
|
36
|
+
});
|
|
37
|
+
const persistLogcatArtifact = ({ details, crashArtifactWriter, testFilePath, }) => {
|
|
38
|
+
if (!crashArtifactWriter || !details.rawLines?.length) {
|
|
39
|
+
return details;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
...details,
|
|
43
|
+
artifactPath: crashArtifactWriter.persistArtifact({
|
|
44
|
+
artifactKind: 'logcat',
|
|
45
|
+
testFilePath,
|
|
46
|
+
source: {
|
|
47
|
+
kind: 'text',
|
|
48
|
+
fileName: 'logcat.txt',
|
|
49
|
+
text: `${details.rawLines.join('\n')}\n`,
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const getDropboxFallbackDetails = (artifact) => ({
|
|
55
|
+
...artifact,
|
|
56
|
+
artifactType: artifact.artifactType,
|
|
57
|
+
artifactPath: artifact.artifactPath,
|
|
58
|
+
});
|
|
59
|
+
const getEnrichmentArtifacts = async ({ bundleId, crashArtifactWriter, getDropboxOutput, getExitInfo, minOccurredAt, occurredAt, pid, testFilePath, }) => {
|
|
60
|
+
const dropboxArtifacts = getDropboxOutput
|
|
61
|
+
? await waitForDropboxArtifacts({
|
|
62
|
+
bundleId,
|
|
63
|
+
crashArtifactWriter,
|
|
64
|
+
getDropboxOutput,
|
|
65
|
+
minOccurredAt,
|
|
66
|
+
occurredAt,
|
|
67
|
+
pid,
|
|
68
|
+
testFilePath,
|
|
69
|
+
})
|
|
70
|
+
: [];
|
|
71
|
+
const enrichmentArtifacts = dropboxArtifacts
|
|
72
|
+
.filter((artifact) => artifact.artifactPath !== undefined)
|
|
73
|
+
.map((artifact) => ({
|
|
74
|
+
artifactType: artifact.artifactType,
|
|
75
|
+
artifactPath: artifact.artifactPath,
|
|
76
|
+
}));
|
|
77
|
+
if (getExitInfo) {
|
|
78
|
+
const exitInfoArtifact = await collectExitInfoArtifact({
|
|
79
|
+
bundleId,
|
|
80
|
+
crashArtifactWriter,
|
|
81
|
+
getExitInfo,
|
|
82
|
+
pid,
|
|
83
|
+
testFilePath,
|
|
84
|
+
});
|
|
85
|
+
if (exitInfoArtifact?.artifactPath.startsWith('/')) {
|
|
86
|
+
enrichmentArtifacts.push({
|
|
87
|
+
artifactType: exitInfoArtifact.artifactType,
|
|
88
|
+
artifactPath: exitInfoArtifact.artifactPath,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { dropboxArtifacts, enrichmentArtifacts };
|
|
93
|
+
};
|
|
94
|
+
export const createAndroidCrashReporter = ({ bundleId, crashArtifactWriter, getLogs, getDropboxOutput, getExitInfo, minOccurredAt, }) => ({
|
|
95
|
+
getCrashDetails: async ({ occurredAt, pid, testFilePath, }) => {
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, CRASH_ARTIFACT_SETTLE_DELAY_MS));
|
|
97
|
+
const rawLines = getCrashBlock(getLogs(), occurredAt);
|
|
98
|
+
const logcatDetails = rawLines.length > 0
|
|
99
|
+
? getLogcatCrashDetails({ rawLines, bundleId, pid })
|
|
100
|
+
: null;
|
|
101
|
+
let enrichmentArtifacts = [];
|
|
102
|
+
let dropboxArtifacts = [];
|
|
103
|
+
if (getDropboxOutput || getExitInfo) {
|
|
104
|
+
({ dropboxArtifacts, enrichmentArtifacts } =
|
|
105
|
+
await getEnrichmentArtifacts({
|
|
106
|
+
bundleId,
|
|
107
|
+
crashArtifactWriter,
|
|
108
|
+
getDropboxOutput,
|
|
109
|
+
getExitInfo,
|
|
110
|
+
minOccurredAt,
|
|
111
|
+
occurredAt,
|
|
112
|
+
pid,
|
|
113
|
+
testFilePath,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
if (logcatDetails && hasUsefulLogcatBlock(rawLines)) {
|
|
117
|
+
return {
|
|
118
|
+
...persistLogcatArtifact({
|
|
119
|
+
details: logcatDetails,
|
|
120
|
+
crashArtifactWriter,
|
|
121
|
+
testFilePath,
|
|
122
|
+
}),
|
|
123
|
+
enrichmentArtifacts: enrichmentArtifacts.length > 0 ? enrichmentArtifacts : undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const dropboxFallback = getBestDropboxArtifact(dropboxArtifacts);
|
|
127
|
+
if (dropboxFallback) {
|
|
128
|
+
const primary = getDropboxFallbackDetails(dropboxFallback);
|
|
129
|
+
return {
|
|
130
|
+
...primary,
|
|
131
|
+
enrichmentArtifacts: enrichmentArtifacts.filter((artifact) => artifact.artifactPath !== primary.artifactPath),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (logcatDetails) {
|
|
135
|
+
return {
|
|
136
|
+
...persistLogcatArtifact({
|
|
137
|
+
details: logcatDetails,
|
|
138
|
+
crashArtifactWriter,
|
|
139
|
+
testFilePath,
|
|
140
|
+
}),
|
|
141
|
+
enrichmentArtifacts: enrichmentArtifacts.length > 0 ? enrichmentArtifacts : undefined,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return enrichmentArtifacts.length > 0
|
|
145
|
+
? {
|
|
146
|
+
enrichmentArtifacts,
|
|
147
|
+
}
|
|
148
|
+
: null;
|
|
149
|
+
},
|
|
150
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-state.d.ts","sourceRoot":"","sources":["../src/device-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,qBAAqB,EAEtB,MAAM,iCAAiC,CAAC;AA0HzC,eAAO,MAAM,wBAAwB,GAAI,SAAS;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAG,qBAqFH,CAAC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as adb from './adb.js';
|
|
2
|
+
const ANDROID_PERMISSION_ALIASES = {
|
|
3
|
+
bluetooth: [
|
|
4
|
+
'android.permission.BLUETOOTH_CONNECT',
|
|
5
|
+
'android.permission.BLUETOOTH_SCAN',
|
|
6
|
+
],
|
|
7
|
+
calendar: [
|
|
8
|
+
'android.permission.READ_CALENDAR',
|
|
9
|
+
'android.permission.WRITE_CALENDAR',
|
|
10
|
+
],
|
|
11
|
+
camera: ['android.permission.CAMERA'],
|
|
12
|
+
contacts: [
|
|
13
|
+
'android.permission.READ_CONTACTS',
|
|
14
|
+
'android.permission.WRITE_CONTACTS',
|
|
15
|
+
],
|
|
16
|
+
location: [
|
|
17
|
+
'android.permission.ACCESS_FINE_LOCATION',
|
|
18
|
+
'android.permission.ACCESS_COARSE_LOCATION',
|
|
19
|
+
],
|
|
20
|
+
mediaLibrary: [
|
|
21
|
+
'android.permission.READ_MEDIA_IMAGES',
|
|
22
|
+
'android.permission.READ_MEDIA_VIDEO',
|
|
23
|
+
'android.permission.READ_MEDIA_AUDIO',
|
|
24
|
+
],
|
|
25
|
+
microphone: ['android.permission.RECORD_AUDIO'],
|
|
26
|
+
notifications: ['android.permission.POST_NOTIFICATIONS'],
|
|
27
|
+
phone: [
|
|
28
|
+
'android.permission.CALL_PHONE',
|
|
29
|
+
'android.permission.ANSWER_PHONE_CALLS',
|
|
30
|
+
],
|
|
31
|
+
sms: [
|
|
32
|
+
'android.permission.READ_SMS',
|
|
33
|
+
'android.permission.RECEIVE_SMS',
|
|
34
|
+
'android.permission.SEND_SMS',
|
|
35
|
+
],
|
|
36
|
+
storage: [
|
|
37
|
+
'android.permission.READ_EXTERNAL_STORAGE',
|
|
38
|
+
'android.permission.WRITE_EXTERNAL_STORAGE',
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
const normalizePermissionName = (name) => {
|
|
42
|
+
return name.trim();
|
|
43
|
+
};
|
|
44
|
+
const resolvePermissions = async (options) => {
|
|
45
|
+
if (options.command.kind === 'permission-all') {
|
|
46
|
+
return adb.getDeclaredDangerousPermissions(options.adbId, options.bundleId);
|
|
47
|
+
}
|
|
48
|
+
const normalized = normalizePermissionName(options.command.permission);
|
|
49
|
+
const aliases = ANDROID_PERMISSION_ALIASES[normalized];
|
|
50
|
+
if (aliases) {
|
|
51
|
+
return aliases;
|
|
52
|
+
}
|
|
53
|
+
return [normalized];
|
|
54
|
+
};
|
|
55
|
+
const snapshotPermissions = async (options) => {
|
|
56
|
+
const granted = await Promise.all(options.permissions.map(async (permission) => {
|
|
57
|
+
const isGranted = await adb.isPermissionGranted(options.adbId, options.bundleId, permission);
|
|
58
|
+
return isGranted ? permission : null;
|
|
59
|
+
}));
|
|
60
|
+
return granted.filter((permission) => permission != null);
|
|
61
|
+
};
|
|
62
|
+
const applyPermissionSnapshot = async (options) => {
|
|
63
|
+
const grantedSet = new Set(options.snapshot.previousGranted);
|
|
64
|
+
const toGrant = options.snapshot.permissions.filter((permission) => grantedSet.has(permission));
|
|
65
|
+
const toRevoke = options.snapshot.permissions.filter((permission) => !grantedSet.has(permission));
|
|
66
|
+
if (toGrant.length > 0) {
|
|
67
|
+
await adb.setPermissions(options.adbId, options.bundleId, toGrant, 'grant', {
|
|
68
|
+
bestEffort: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (toRevoke.length > 0) {
|
|
72
|
+
await adb.setPermissions(options.adbId, options.bundleId, toRevoke, 'reset', {
|
|
73
|
+
bestEffort: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
export const createAndroidDeviceState = (options) => {
|
|
78
|
+
const outstandingMutations = new Map();
|
|
79
|
+
let mutationCounter = 0;
|
|
80
|
+
const rememberMutation = (snapshot) => {
|
|
81
|
+
const id = `android-permissions-${++mutationCounter}`;
|
|
82
|
+
const mutation = {
|
|
83
|
+
id,
|
|
84
|
+
snapshot,
|
|
85
|
+
revert: async () => {
|
|
86
|
+
await controller.permissions.revert(id);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
outstandingMutations.set(id, mutation);
|
|
90
|
+
return mutation;
|
|
91
|
+
};
|
|
92
|
+
const controller = {
|
|
93
|
+
permissions: {
|
|
94
|
+
apply: async (command) => {
|
|
95
|
+
const permissions = await resolvePermissions({
|
|
96
|
+
adbId: options.adbId,
|
|
97
|
+
bundleId: options.bundleId,
|
|
98
|
+
command,
|
|
99
|
+
});
|
|
100
|
+
if (permissions.length === 0) {
|
|
101
|
+
return { mutation: null };
|
|
102
|
+
}
|
|
103
|
+
const previousGranted = await snapshotPermissions({
|
|
104
|
+
adbId: options.adbId,
|
|
105
|
+
bundleId: options.bundleId,
|
|
106
|
+
permissions,
|
|
107
|
+
});
|
|
108
|
+
await adb.setPermissions(options.adbId, options.bundleId, permissions, command.decision, { bestEffort: command.kind === 'permission-all' });
|
|
109
|
+
const mutation = rememberMutation({
|
|
110
|
+
kind: 'permission-set',
|
|
111
|
+
permissions,
|
|
112
|
+
previousGranted,
|
|
113
|
+
});
|
|
114
|
+
return { mutation };
|
|
115
|
+
},
|
|
116
|
+
revert: async (mutationId) => {
|
|
117
|
+
const mutation = outstandingMutations.get(mutationId);
|
|
118
|
+
if (!mutation) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
outstandingMutations.delete(mutationId);
|
|
122
|
+
await applyPermissionSnapshot({
|
|
123
|
+
adbId: options.adbId,
|
|
124
|
+
bundleId: options.bundleId,
|
|
125
|
+
snapshot: mutation.snapshot,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
resetOutstanding: async () => {
|
|
129
|
+
const mutations = [...outstandingMutations.values()].reverse();
|
|
130
|
+
outstandingMutations.clear();
|
|
131
|
+
for (const mutation of mutations) {
|
|
132
|
+
await applyPermissionSnapshot({
|
|
133
|
+
adbId: options.adbId,
|
|
134
|
+
bundleId: options.bundleId,
|
|
135
|
+
snapshot: mutation.snapshot,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
return controller;
|
|
142
|
+
};
|
package/dist/instance.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ import { type HarnessPlatformInitOptions, HarnessPlatformRunner } from '@react-n
|
|
|
2
2
|
import type { Config as HarnessConfig } from '@react-native-harness/config';
|
|
3
3
|
import { AndroidPlatformConfig } from './config.js';
|
|
4
4
|
export declare const getAndroidEmulatorPlatformInstance: (config: AndroidPlatformConfig, harnessConfig: HarnessConfig, init: HarnessPlatformInitOptions) => Promise<HarnessPlatformRunner>;
|
|
5
|
-
export declare const getAndroidPhysicalDevicePlatformInstance: (config: AndroidPlatformConfig, harnessConfig: HarnessConfig) => Promise<HarnessPlatformRunner>;
|
|
5
|
+
export declare const getAndroidPhysicalDevicePlatformInstance: (config: AndroidPlatformConfig, harnessConfig: HarnessConfig, init?: HarnessPlatformInitOptions) => Promise<HarnessPlatformRunner>;
|
|
6
6
|
//# sourceMappingURL=instance.d.ts.map
|
package/dist/instance.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,0BAA0B,EAC/B,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE5E,OAAO,EACL,qBAAqB,EAGtB,MAAM,aAAa,CAAC;AAuKrB,eAAO,MAAM,kCAAkC,GAC7C,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,EAC5B,MAAM,0BAA0B,KAC/B,OAAO,CAAC,qBAAqB,CA8H/B,CAAC;AAEF,eAAO,MAAM,wCAAwC,GACnD,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,EAC5B,OAAO,0BAA0B,KAChC,OAAO,CAAC,qBAAqB,CAyD/B,CAAC"}
|