Skip to content

Commit

Permalink
feat: Update console and network log handlers (#2421)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Jul 2, 2024
1 parent 0f45dd5 commit 3c72721
Show file tree
Hide file tree
Showing 15 changed files with 274 additions and 451 deletions.
2 changes: 2 additions & 0 deletions docs/reference/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ about capabilities, refer to the [Appium documentation](https://appium.io/docs/e
|`appium:autoWebview`| Move directly into Webview context if available. Default `false`|`true`, `false`|
|`appium:skipTriggerInputEventAfterSendkeys`| If this capability is set to `true`, then whenever you call the Send Keys method in a web context, the driver will not fire an additional `input` event on the input field used for the call. This event, turned on by default, helps in situations where JS frameworks (like React) do not respond to the input events that occur by default when the underlying Selenium atom is executed. Default `false`|`true`, `false`|
|`appium:sendKeyStrategy`| If this capability is set to `oneByOne`, then whenever you call the Send Keys method in a web context, the driver will type each character the given string consists of in serial order to the element. This strategy helps in situations where JS frameworks (like React) update the view for each input. If `appium:skipTriggerInputEventAfterSendkeys` capability is `true`, it will affect every type. For example, when you are going to type the word `appium` with `oneByOne` strategy and `appium:skipTriggerInputEventAfterSendkeys` is enabled, the `appium:skipTriggerInputEventAfterSendkeys` option affects each typing action: `a`, `p`, `p`,`i`, `u` and `m`. Suppose any other value or no value has been provided to the `appium:sendKeyStrategy` capability. In that case, the driver types the given string in the destination input element. `appium` Send Keys input types `appium` if `oneByOne` was not set. |`oneByOne`|
|`appium:showSafariConsoleLog`| Adds Safari JavaScript console events to Appium server logs (`true`) and writes fully serialized events into the `safariConsole` logs bucket (both `true` and `false`). If unset then no console events are being collected, which helps to save CPU and memory resources. Before the driver version 7.22 the default behavior was to always collect console logs if the capability is not set. Setting the value to `false` mimics that legacy behavior. |`true`, `false`|
|`appium:showSafariNetworkLog`| Adds Safari network events to Appium server logs (`true`) and writes fully serialized events into the `safariNetwork` logs bucket (both `true` and `false`). If unset then no network events are being collected, which helps to save CPU and memory resources. Before the driver version 7.22 the default behavior was to always collect network logs if the capability is not set. Setting the value to `false` mimics that legacy behavior. |`true`, `false`|

### Other

Expand Down
4 changes: 2 additions & 2 deletions lib/commands/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -569,12 +569,12 @@ const commands = {
if (name && name !== NATIVE_WIN && this.logs) {
if (this.logs.safariConsole) {
await this.remote.startConsole(
this.logs.safariConsole.addLogLine.bind(this.logs.safariConsole),
this.logs.safariConsole.onConsoleLogEvent.bind(this.logs.safariConsole),
);
}
if (this.logs.safariNetwork) {
await this.remote.startNetwork(
this.logs.safariNetwork.addLogLine.bind(this.logs.safariNetwork),
this.logs.safariNetwork.onNetworkEvent.bind(this.logs.safariNetwork),
);
}
}
Expand Down
46 changes: 32 additions & 14 deletions lib/commands/log.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ const SUPPORTED_LOG_TYPES = {
},
};

const LOG_NAMES_TO_CAPABILITY_NAMES_MAP = {
safariConsole: 'showSafariConsoleLog',
safariNetwork: 'showSafariNetworkLog',
enablePerformanceLogging: 'enablePerformanceLogging',
};

export default {
supportedLogTypes: SUPPORTED_LOG_TYPES,
/**
Expand All @@ -77,11 +83,18 @@ export default {

// If logs captured successfully send response with data, else send error
const logObject = logsContainer[logType];
const logs = logObject ? await logObject.getLogs() : null;
if (logs) {
return logs;
if (logObject) {
return await logObject.getLogs();
}
if (logType in LOG_NAMES_TO_CAPABILITY_NAMES_MAP) {
throw new Error(
`${logType} logs are not enabled. Make sure you've set a proper value ` +
`to the 'appium:${LOG_NAMES_TO_CAPABILITY_NAMES_MAP[logType]}' capability.`
);
}
throw new Error(`No logs of type '${String(logType)}' found.`);
throw new Error(
`No logs of type '${logType}' found. Supported log types are: ${_.keys(SUPPORTED_LOG_TYPES)}.`
);
},

/**
Expand All @@ -98,23 +111,30 @@ export default {
sim: this.device,
udid: this.isRealDevice() ? this.opts.udid : undefined,
});

if (this.isRealDevice()) {
this.logs.syslog = new IOSDeviceLog({
this.logs.syslog = this.isRealDevice()
? new IOSDeviceLog({
udid: this.opts.udid,
showLogs: this.opts.showIOSLog,
log: this.log,
});
} else {
this.logs.syslog = new IOSSimulatorLog({
})
: new IOSSimulatorLog({
sim: /** @type {import('appium-ios-simulator').Simulator} */ (this.device),
showLogs: this.opts.showIOSLog,
iosSimulatorLogsPredicate: this.opts.iosSimulatorLogsPredicate,
log: this.log,
});
if (_.isBoolean(this.opts.showSafariConsoleLog)) {
this.logs.safariConsole = new SafariConsoleLog({
showLogs: this.opts.showSafariConsoleLog,
log: this.log,
});
}
if (_.isBoolean(this.opts.showSafariNetworkLog)) {
this.logs.safariNetwork = new SafariNetworkLog({
showLogs: this.opts.showSafariNetworkLog,
log: this.log,
});
}
this.logs.safariConsole = new SafariConsoleLog(!!this.opts.showSafariConsoleLog);
this.logs.safariNetwork = new SafariNetworkLog(!!this.opts.showSafariNetworkLog);
}

let didStartSyslog = false;
Expand All @@ -130,8 +150,6 @@ export default {
}
})(),
this.logs.crashlog.startCapture(),
this.logs.safariConsole.startCapture(),
this.logs.safariNetwork.startCapture(),
];
await B.all(promises);

Expand Down
8 changes: 6 additions & 2 deletions lib/device-log/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { LogEntry } from '../commands/types';

export function toLogEntry(message: string, timestamp: number): LogEntry {
export const DEFAULT_LOG_LEVEL = 'ALL';
export const MAX_JSON_LOG_LENGTH = 200;
export const MAX_BUFFERED_EVENTS_COUNT = 5000;

export function toLogEntry(message: string, timestamp: number, level: string = DEFAULT_LOG_LEVEL): LogEntry {
return {
timestamp,
level: 'ALL',
level,
message,
};
}
9 changes: 0 additions & 9 deletions lib/device-log/ios-crash-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,6 @@ class IOSCrashLog {
return await this.filesToJSON(diff);
}

/**
* @returns {Promise<import('../commands/types').LogEntry[]>}
*/
async getAllLogs() {
let crashFiles = await this.getCrashes();
let logFiles = _.difference(crashFiles, this.prevLogs);
return await this.filesToJSON(logFiles);
}

/**
* @param {string[]} paths
* @returns {Promise<import('../commands/types').LogEntry[]>}
Expand Down
6 changes: 3 additions & 3 deletions lib/device-log/ios-device-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import type { AppiumLogger } from '@appium/types';
export interface IOSDeviceLogOpts {
udid: string;
showLogs?: boolean;
log?: AppiumLogger;
log: AppiumLogger;
}

export class IOSDeviceLog extends LineConsumingLog {
private udid: string;
private showLogs: boolean;
private readonly udid: string;
private readonly showLogs: boolean;
private service: any | null;

constructor(opts: IOSDeviceLogOpts) {
Expand Down
49 changes: 16 additions & 33 deletions lib/device-log/ios-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export abstract class IOSLog<
> extends EventEmitter {
private maxBufferSize: number;
private logs: LRUCache<number, TSerializedEntry>;
private logIndexSinceLastRequest: number | null;
private _log: AppiumLogger;

constructor(opts: IOSLogOptions = {}) {
Expand All @@ -27,7 +26,6 @@ export abstract class IOSLog<
this.logs = new LRUCache({
max: this.maxBufferSize,
});
this.logIndexSinceLastRequest = null;
this._log = opts.log ?? logger.getLogger(this.constructor.name);
}

Expand All @@ -39,40 +37,12 @@ export abstract class IOSLog<
return this._log;
}

broadcast(entry: TRawEntry): void {
let recentIndex = -1;
for (const key of this.logs.rkeys()) {
recentIndex = key;
break;
}
const serializedEntry = this._serializeEntry(entry);
this.logs.set(++recentIndex, serializedEntry);
if (this.listenerCount('output')) {
this.emit('output', this._deserializeEntry(serializedEntry));
}
}

getLogs(): LogEntry[] {
const result: LogEntry[] = [];
let recentLogIndex: number | null = null;
for (const [index, value] of this.logs.entries()) {
if (this.logIndexSinceLastRequest && index > this.logIndexSinceLastRequest
|| !this.logIndexSinceLastRequest) {
recentLogIndex = index;
result.push(this._deserializeEntry(value));
}
}
if (recentLogIndex !== null) {
this.logIndexSinceLastRequest = recentLogIndex;
}
return result;
}

getAllLogs(): LogEntry[] {
const result: LogEntry[] = [];
for (const value of this.logs.values()) {
result.push(this._deserializeEntry(value));
for (const value of this.logs.rvalues()) {
result.push(this._deserializeEntry(value as TSerializedEntry));
}
this._clearEntries();
return result;
}

Expand All @@ -82,6 +52,19 @@ export abstract class IOSLog<
protected _clearEntries() {
this.logs.clear();
}

protected broadcast(entry: TRawEntry): void {
let recentIndex = -1;
for (const key of this.logs.keys()) {
recentIndex = key;
break;
}
const serializedEntry = this._serializeEntry(entry);
this.logs.set(++recentIndex, serializedEntry);
if (this.listenerCount('output')) {
this.emit('output', this._deserializeEntry(serializedEntry));
}
}
}

export default IOSLog;
33 changes: 11 additions & 22 deletions lib/device-log/ios-performance-log.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import _ from 'lodash';
import { IOSLog } from './ios-log';
import type { LogEntry } from '../commands/types';
import type { AppiumLogger } from '@appium/types';

const MAX_EVENTS = 5000;
import { MAX_JSON_LOG_LENGTH, MAX_BUFFERED_EVENTS_COUNT } from './helpers';
import { LineConsumingLog } from './line-consuming-log';

type PerformanceLogEntry = object;
export interface IOSPerformanceLogOptions {
remoteDebugger: any;
maxEvents?: number;
log?: AppiumLogger;
log: AppiumLogger;
}

export class IOSPerformanceLog extends IOSLog<PerformanceLogEntry, PerformanceLogEntry> {
private remoteDebugger: any;
export class IOSPerformanceLog extends LineConsumingLog {
private readonly remoteDebugger: any;
private _started: boolean;

constructor(opts: IOSPerformanceLogOptions) {
super({
maxBufferSize: opts.maxEvents ?? MAX_EVENTS,
maxBufferSize: opts.maxEvents ?? MAX_BUFFERED_EVENTS_COUNT,
log: opts.log,
});
this.remoteDebugger = opts.remoteDebugger;
Expand All @@ -28,33 +26,24 @@ export class IOSPerformanceLog extends IOSLog<PerformanceLogEntry, PerformanceLo
override async startCapture(): Promise<void> {
this.log.debug('Starting performance (Timeline) log capture');
this._clearEntries();
const result = await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this));
await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this));
this._started = true;
return result;
}

override async stopCapture(): Promise<void> {
this.log.debug('Stopping performance (Timeline) log capture');
const result = await this.remoteDebugger.stopTimeline();
await this.remoteDebugger.stopTimeline();
this._started = false;
return result;
}

override get isCapturing(): boolean {
return this._started;
}

protected override _serializeEntry(value: PerformanceLogEntry): PerformanceLogEntry {
return value;
}

protected override _deserializeEntry(value: PerformanceLogEntry): LogEntry {
return value as LogEntry;
}

private onTimelineEvent(event: PerformanceLogEntry): void {
this.log.debug(`Received Timeline event: ${_.truncate(JSON.stringify(event))}`);
this.broadcast(event);
const serializedEntry = JSON.stringify(event);
this.broadcast(serializedEntry);
this.log.debug(`Received Timeline event: ${_.truncate(serializedEntry, {length: MAX_JSON_LOG_LENGTH})}`);
}
}

Expand Down
20 changes: 9 additions & 11 deletions lib/device-log/ios-simulator-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ export interface IOSSimulatorLogOptions {
sim: Simulator;
showLogs?: boolean;
iosSimulatorLogsPredicate?: string;
log?: AppiumLogger;
log: AppiumLogger;
}

export class IOSSimulatorLog extends LineConsumingLog {
private sim: Simulator;
private showLogs: boolean;
private predicate?: string;
private readonly sim: Simulator;
private readonly showLogs: boolean;
private readonly predicate?: string;
private proc: SubProcess | null;

constructor(opts: IOSSimulatorLogOptions) {
Expand Down Expand Up @@ -93,24 +93,22 @@ export class IOSSimulatorLog extends LineConsumingLog {
}
}

private async finishStartingLogCapture() {
private async finishStartingLogCapture(): Promise<void> {
if (!this.proc) {
throw this.log.errorWithException('Could not capture simulator log');
}

for (const streamName of ['stdout', 'stderr']) {
this.proc.on(`lines-${streamName}`, (/** @type {string[]} */ lines) => {
for (const line of lines) {
this.onOutput(line, ...(streamName === 'stderr' ? ['STDERR'] : []));
}
this.proc.on(`line-${streamName}`, (line: string) => {
this.onOutput(line, ...(streamName === 'stderr' ? ['STDERR'] : []));
});
}

const startDetector = (/** @type {string} */ stdout, /** @type {string} */ stderr) => {
const startDetector = (stdout: string, stderr: string) => {
if (EXECVP_ERROR_PATTERN.test(stderr)) {
throw new Error('iOS log capture process failed to start');
}
return stdout || stderr;
return Boolean(stdout || stderr);
};
await this.proc.start(startDetector, START_TIMEOUT);
}
Expand Down
Loading

0 comments on commit 3c72721

Please sign in to comment.