Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 54 additions & 47 deletions src/logs/log-buffer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// eslint-disable-next-line import/no-internal-modules
import type { AllureRuntime } from 'jest-allure2-reporter/api';

import type { Emitter, AndroidEntry, IosEntry, Entry } from 'logkitten';
import type { Emitter, AndroidEntry, IosEntry } from 'logkitten';
import { Level, logkitten } from 'logkitten';
import type { DetoxAllure2AdapterDeviceLogsOptions } from '../types';
import type { DeviceWrapper } from '../utils';

import { PIDEntryCollection } from './pid-entry-collection';

type AnyEntry = AndroidEntry & IosEntry;

export interface LogBufferOptions {
Expand All @@ -28,9 +30,8 @@ const noop = () => {};

export class LogBuffer implements StepLogRecorder {
private readonly _emitter: Emitter;
private _entries: AnyEntry[] = [];
private _purgatory: AnyEntry[] = [];
private _pid = Number.NaN;
private readonly _appEntries = new PIDEntryCollection();
private readonly _detoxEntries = new PIDEntryCollection();
private _options: DetoxAllure2AdapterDeviceLogsOptions;

constructor(readonly _config: LogBufferOptions) {
Expand All @@ -57,41 +58,14 @@ export class LogBuffer implements StepLogRecorder {
}

public resetPid() {
this._pid = Number.NaN;
this._appEntries.pid = Number.NaN;
this._detoxEntries.pid = Number.NaN;
}

public refreshPid() {
this._pid = this._config.device.getPid();

if (Number.isFinite(this._pid) && this._purgatory.length > 0) {
this._entries = [...this._entries, ...this._purgatory.splice(0).filter(this._matchesPid)];
}
}

public flush(failed?: boolean): string {
if (this._entries.length === 0) {
return '';
}

const entries = this._entries.splice(0);

// Check if we should save logs based on failure status
const saveAll = this._options.saveAll ?? false;
if (!saveAll && !failed) {
return '';
}

const result = entries
.map((entry) => {
const levelLetter = Level[entry.level as Level] || 'UNKNOWN';
const tagOrCategory = entry.tag || `${entry.subsystem}:${entry.category}`;
const msg = entry.msg;

return `${levelLetter}\t${tagOrCategory}\t${msg}`;
})
.join('\n');

return result + '\n';
const pid = this._config.device.getPid();
this._appEntries.pid = pid;
this._detoxEntries.pid = pid;
}

public async close() {
Expand All @@ -116,25 +90,39 @@ export class LogBuffer implements StepLogRecorder {
}

private readonly _attachLogs = (allure: AllureRuntime, failed: boolean, after: boolean) => {
const content = this.flush(failed);
// Check if we should save logs based on failure status
const saveAll = this._options.saveAll ?? false;
if (!saveAll && !failed) {
return;
}

if (content) {
const appContent = this._appEntries.flushAsString();
if (appContent) {
const name = after ? 'app.log' : 'app-before.log';
allure.attachment(name, content, 'text/plain');
allure.attachment(name, appContent, 'text/plain');
}
};

private readonly _onEntry = (entry: AnyEntry) => {
if (Number.isFinite(this._pid)) {
this._entries.push(entry);
} else {
this._purgatory.push(entry);
const detoxContent = this._detoxEntries.flushAsString();
if (detoxContent) {
const name = after ? 'detox.log' : 'detox-before.log';
allure.attachment(name, detoxContent, 'text/plain');
}
};

private readonly _matchesPid = (entry: Entry) => entry.pid === this._pid;
private readonly _onEntry = (entry: AnyEntry) => {
this._appEntries.push(entry);
};

private _iosFilter(entry: IosEntry): boolean {
if (entry.subsystem === 'com.wix.Detox') {
this._detoxEntries.push(entry);

// Exclude Detox logs from app logs unless they are errors
if (entry.level < Level.ERROR) {
return false;
}
}

const userFilter = this._options.ios;
const override = this._options.override;
if (!override && !this._defaultIosFilter(entry)) {
Expand All @@ -145,6 +133,15 @@ export class LogBuffer implements StepLogRecorder {
}

private _androidFilter(entry: AndroidEntry): boolean {
if (entry.tag && entry.tag.startsWith('Detox')) {
this._detoxEntries.push(entry);

// Exclude Detox logs from app logs unless they are errors
if (entry.level < Level.ERROR) {
return false;
}
}

const userFilter = this._options.android;
const override = this._options.override;
if (!override && !this._defaultAndroidFilter(entry)) {
Expand All @@ -155,10 +152,19 @@ export class LogBuffer implements StepLogRecorder {
}

private readonly _defaultIosFilter = (entry: IosEntry) => {
// Only handle React Native app logs, not Detox logs
if (entry.subsystem.startsWith('com.facebook.react.')) {
if (entry.msg.startsWith('Unbalanced calls start/end for tag')) {
return false;
}

return true;
}

if (entry.processImagePath.endsWith('/proactiveeventtrackerd')) {
return false;
}

if (entry.level >= Level.ERROR) {
return !entry.subsystem.startsWith('com.apple.'); // && !entry.msg.includes('(CFNetwork)');
}
Expand All @@ -167,7 +173,8 @@ export class LogBuffer implements StepLogRecorder {
};

private readonly _defaultAndroidFilter = (entry: AndroidEntry) => {
if (entry.tag.startsWith('Detox') || entry.tag.startsWith('React')) {
// Only handle React Native app logs, not Detox logs
if (entry.tag.startsWith('React')) {
return true;
}

Expand Down
57 changes: 57 additions & 0 deletions src/logs/pid-entry-collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// eslint-disable-next-line import/no-internal-modules
import type { AndroidEntry, Entry, IosEntry } from 'logkitten';
import { Level } from 'logkitten';

export class PIDEntryCollection {
private _entries: Entry[] = [];
private _purgatory: Entry[] = [];
private _pid: number = Number.NaN;
private _current: Entry[] = this._purgatory;

get pid(): number {
return this._pid;
}

set pid(value: number) {
this._pid = value;
this._current = Number.isFinite(this._pid) ? this._entries : this._purgatory;

if (this._purgatory.length > 0) {
const queue = this._purgatory.splice(0);
if (this._current === this._entries) {
this._entries.push(...queue);
}
}
}

public push(entry: Entry): void {
this._current.push(entry);
}

public flushAsString(): string {
const entries = this._flush();
if (entries.length === 0) {
return '';
}

const result = entries
.map((entry) => {
const level = Level[entry.level as Level] || 'UNKNOWN';
const tagOrCategory = entry.tag || join2(entry.subsystem, entry.category);
const msg = entry.msg.replace(/\n/g, '\n\t\t');

return `${level}\t${tagOrCategory}\t${msg}`;
})
.join('\n');

return result + '\n';
}

private _flush() {
return this._entries.splice(0) as (AndroidEntry & IosEntry)[];
}
}

function join2(a: string, b: string): string {
return a && b ? `${a}:${b}` : a || b || '';
}

Check failure on line 57 in src/logs/pid-entry-collection.ts

View workflow job for this annotation

GitHub Actions / Sanity

Insert `⏎`
Loading