Skip to content

Commit

Permalink
feat(ado-improvement-1): Intercept & adjust logging in scan process (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
DaveTryon committed Feb 8, 2022
1 parent fd50d74 commit a27ffb2
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/ado-extension/src/ado-extension.ts
Expand Up @@ -5,11 +5,16 @@ import 'reflect-metadata';
import './module-name-mapper';

import { Logger } from '@accessibility-insights-action/shared';
import { hookStderr } from '@accessibility-insights-action/shared';
import { hookStdout } from '@accessibility-insights-action/shared';
import { Scanner } from '@accessibility-insights-action/shared';
import { setupIocContainer } from './ioc/setup-ioc-container';

export function runScan() {
(async () => {
hookStderr();
hookStdout();

const container = setupIocContainer();
const logger = container.get(Logger);
await logger.setup();
Expand Down
5 changes: 5 additions & 0 deletions packages/gh-action/src/index.ts
Expand Up @@ -4,10 +4,15 @@ import 'reflect-metadata';
import './module-name-mapper';

import { Logger } from '@accessibility-insights-action/shared';
import { hookStderr } from '@accessibility-insights-action/shared';
import { hookStdout } from '@accessibility-insights-action/shared';
import { Scanner } from '@accessibility-insights-action/shared';
import { setupIocContainer } from './ioc/setup-ioc-container';

(async () => {
hookStderr();
hookStdout();

const container = setupIocContainer();
const logger = container.get(Logger);
await logger.setup();
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/index.ts
Expand Up @@ -13,3 +13,5 @@ export { checkRunDetailsTitle, checkRunName } from './content/strings';
export { TaskConfig } from './task-config';
export { BaselineInfo } from './baseline-info';
export { ArtifactsInfoProvider } from './artifacts-info-provider';
export { hookStdout } from './output-hooks/hook-stdout';
export { hookStderr } from './output-hooks/hook-stderr';
10 changes: 10 additions & 0 deletions packages/shared/src/output-hooks/hook-stderr.ts
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { stderr } from 'process';
import { hookStream } from './hook-stream';
import { stderrTransformer } from './stderr-transformer';

export const hookStderr = (): (() => void) => {
return hookStream(stderr, stderrTransformer);
};
10 changes: 10 additions & 0 deletions packages/shared/src/output-hooks/hook-stdout.ts
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { stdout } from 'process';
import { hookStream } from './hook-stream';
import { stdoutTransformer } from './stdout-transformer';

export const hookStdout = (): (() => void) => {
return hookStream(stdout, stdoutTransformer);
};
105 changes: 105 additions & 0 deletions packages/shared/src/output-hooks/hook-stream.spec.ts
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { hookStream } from './hook-stream';
import { Writable } from 'stream';

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */

const defaultEncoding = 'buffer';

type WriteCall = {
chunk: any;
encoding: BufferEncoding;
callback: (error?: Error | null) => void;
};

// This class simply records the contents of any _write calls for validation
class TestStream extends Writable {
private writeCalls: WriteCall[] = [];

_write = (chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void => {
this.writeCalls.push({ chunk, encoding, callback });
};

getWriteCalls(): WriteCall[] {
return this.writeCalls;
}
}

describe(hookStream, () => {
let stream: TestStream;

function testTransformer(input: string): string | null {
switch (input) {
case 'return null':
return null;
case 'return xyz':
return 'xyz';
}

return input;
}

beforeEach(() => {
stream = new TestStream();
});

it.each`
input
${'abc'}
${'return xyz'}
${'return null'}
`(`with hook never enabled, input value '$input' writes unchanged'`, ({ input }) => {
stream.write(input);

const writeCalls = stream.getWriteCalls();

expect(writeCalls.length).toBe(1);
expect(String(writeCalls[0].chunk)).toBe(input);
expect(writeCalls[0].encoding).toBe(defaultEncoding);
expect(writeCalls[0].callback).toBeTruthy();
});

it.each`
input | expectedOutput
${'abc'} | ${'abc'}
${'return xyz'} | ${'xyz'}
${'return null'} | ${null}
`(`with hook enabled, input value '$input' writes as '$expectedOutput'`, ({ input, expectedOutput }) => {
hookStream(stream as unknown as NodeJS.WriteStream, testTransformer);

stream.write(input);

const writeCalls = stream.getWriteCalls();

if (expectedOutput) {
expect(writeCalls.length).toBe(1);
expect(String(writeCalls[0].chunk)).toBe(expectedOutput);
expect(writeCalls[0].encoding).toBe(defaultEncoding);
expect(writeCalls[0].callback).toBeTruthy();
} else {
expect(writeCalls.length).toBe(0);
}
});

it.each`
input
${'abc'}
${'return xyz'}
${'return null'}
`(`with hook enabled then disabled, input value '$input' writes unchanged'`, ({ input }) => {
const unhook = hookStream(stream as unknown as NodeJS.WriteStream, testTransformer);
unhook();

stream.write(input);

const writeCalls = stream.getWriteCalls();

expect(writeCalls.length).toBe(1);
expect(String(writeCalls[0].chunk)).toBe(input);
expect(writeCalls[0].encoding).toBe(defaultEncoding);
expect(writeCalls[0].callback).toBeTruthy();
});
});
30 changes: 30 additions & 0 deletions packages/shared/src/output-hooks/hook-stream.ts
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-explicit-any */

export type StreamTransformer = (data: string) => string;

// This method hooks a stream at its _write method, which is the lowest level that
// is exposed via the interface. Calling the method returned from hookStream
// removes the hook by restoring the previous _write method.
export const hookStream = (stream: NodeJS.WriteStream, transformer: (rawData: string) => string): (() => void) => {
const oldWrite = stream._write;

const unhook = () => {
stream._write = oldWrite;
};

const newWrite = (chunk: any, encoding: BufferEncoding, callback: (err?: Error) => void) => {
const transformedValue = transformer(String(chunk));

if (transformedValue) {
oldWrite.call(stream, transformedValue, encoding, callback);
}
};

stream._write = newWrite;

return unhook;
};
20 changes: 20 additions & 0 deletions packages/shared/src/output-hooks/stderr-transformer.spec.ts
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { stderrTransformer } from './stderr-transformer';

describe(stderrTransformer, () => {
it.each`
input | expectedOutput
${'abc'} | ${'abc'}
${'waitFor is deprecate'} | ${'waitFor is deprecate'}
${'waitFor is deprecated'} | ${null}
${'waitFor is deprecated abc'} | ${null}
${'Some icons were re-registered'} | ${'Some icons were re-registered'}
${'Some icons were re-registered.'} | ${null}
${'Some icons were re-registered. abc'} | ${null}
`(`input value '$input' returned as '$expectedOutput'`, ({ input, expectedOutput }) => {
const output = stderrTransformer(input);
expect(output).toBe(expectedOutput);
});
});
9 changes: 9 additions & 0 deletions packages/shared/src/output-hooks/stderr-transformer.ts
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

export const stderrTransformer = (rawData: string): string => {
if (rawData.startsWith('waitFor is deprecated')) return null;
if (rawData.startsWith('Some icons were re-registered.')) return null;

return rawData;
};
39 changes: 39 additions & 0 deletions packages/shared/src/output-hooks/stdout-transformer.spec.ts
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { stdoutTransformer } from './stdout-transformer';

describe(stdoutTransformer, () => {
it.each`
input | expectedOutput
${'abc'} | ${'##[debug] abc'}
${'[Trace][info] ==='} | ${'##[debug] [Trace][info] ==='}
${'\u001B[32mINFO\u001b[39m'} | ${'##[debug] \u001B[32mINFO\u001b[39m'}
${'Processing page'} | ${'##[debug] Processing page'}
${'Discovered 12 links on'} | ${'##[debug] Discovered 12 links on'}
`(`Debug tag added to raw input - input value '$input' returned as '$expectedOutput'`, ({ input, expectedOutput }) => {
const output = stdoutTransformer(input);
expect(output).toBe(expectedOutput);
});

it.each`
input | expectedOutput
${'[Trace][info] === '} | ${'##[debug] '}
${'[Trace][info] === abc'} | ${'##[debug] abc'}
${'\u001B[32mINFO\u001b[39m '} | ${'##[debug] '}
${'\u001B[32mINFO\u001b[39m abc'} | ${'##[debug] abc'}
`(`Debug tag added to modified input - input value '$input' returned as '$expectedOutput'`, ({ input, expectedOutput }) => {
const output = stdoutTransformer(input);
expect(output).toBe(expectedOutput);
});

it.each`
input | expectedOutput
${'##[error] abc'} | ${'##[error] abc'}
${'##[debug] abc'} | ${'##[debug] abc'}
${'##vso[task.debug] abc'} | ${'##vso[task.debug] abc'}
`(`Debug tag not added - input value '$input' returned as '$expectedOutput'`, ({ input, expectedOutput }) => {
const output = stdoutTransformer(input);
expect(output).toBe(expectedOutput);
});
});
74 changes: 74 additions & 0 deletions packages/shared/src/output-hooks/stdout-transformer.ts
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

const debugPrefix = '##[debug]';

type RegexTransformation = {
regex: RegExp;
method: (rawData: string, regex?: RegExp) => string;
};

const regexTransformations: RegexTransformation[] = [
{
regex: new RegExp('^\\[Exception\\]'),
method: useUnmodifiedString,
},
{
regex: new RegExp('^##\\[error\\]'),
method: useUnmodifiedString,
},
{
regex: new RegExp('^##\\[debug\\]'),
method: useUnmodifiedString,
},
{
regex: new RegExp('^##vso\\[task.debug\\]'),
method: useUnmodifiedString,
},
{
regex: new RegExp('^\\[Trace\\]\\[info\\] === '),
method: replaceFirstMatchWithDebugPrefix,
},
{
// eslint-disable-next-line no-control-regex
regex: new RegExp('^\u001B\\[32mINFO\u001b\\[39m '), // Includes escape characters used for color formatting)
method: replaceFirstMatchWithDebugPrefix,
},
];

export const stdoutTransformer = (rawData: string): string => {
for (const startSubstitution of regexTransformations) {
const newData = evaluateRegexTransformation(rawData, startSubstitution.regex, startSubstitution.method);

if (newData) {
return newData;
}
}

return prependDebugPrefix(rawData);
};

export function evaluateRegexTransformation(
input: string,
regex: RegExp,
modifier: (input: string, regex?: RegExp) => string,
): string | null {
const matches = input.match(regex);
if (matches) {
return modifier(input, regex);
}

return null;
}

function useUnmodifiedString(input: string): string {
return input;
}

function replaceFirstMatchWithDebugPrefix(input: string, regex: RegExp): string {
return `${debugPrefix} ${input.replace(regex, '$`')}`;
}

function prependDebugPrefix(input: string): string {
return `${debugPrefix} ${input}`;
}

0 comments on commit a27ffb2

Please sign in to comment.