Use unique keys to identify healthchecks

Summary: Added unique keys for each healthcheck and used them to save already seen failures. Later I will also use them for gathering doctor analytics.

Reviewed By: jknoxville

Differential Revision: D19301583

fbshipit-source-id: 0c0aa977ea73ce555e0d9fb8e8ead844624fb9cd
This commit is contained in:
Anton Nikolaev
2020-01-08 08:10:36 -08:00
committed by Facebook Github Bot
parent cdb0990ac8
commit 751c778069
10 changed files with 560 additions and 249 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "flipper-doctor", "name": "flipper-doctor",
"version": "0.5.0", "version": "0.6.0",
"description": "Utility for checking for issues with a flipper installation", "description": "Utility for checking for issues with a flipper installation",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",

View File

@@ -22,7 +22,8 @@ import {getEnvInfo} from './environmentInfo';
: { : {
label: category.label, label: category.label,
results: await Promise.all( results: await Promise.all(
category.healthchecks.map(async ({label, run}) => ({ category.healthchecks.map(async ({key, label, run}) => ({
key,
label, label,
result: await run(environmentInfo), result: await run(environmentInfo),
})), })),

View File

@@ -33,6 +33,7 @@ export type Healthchecks = {
}; };
export type Healthcheck = { export type Healthcheck = {
key: string;
label: string; label: string;
isRequired?: boolean; isRequired?: boolean;
run: ( run: (
@@ -48,6 +49,7 @@ export type CategoryResult = [
{ {
label: string; label: string;
results: Array<{ results: Array<{
key: string;
label: string; label: string;
isRequired: boolean; isRequired: boolean;
result: {hasProblem: boolean}; result: {hasProblem: boolean};
@@ -63,6 +65,7 @@ export function getHealthchecks(): Healthchecks {
isSkipped: false, isSkipped: false,
healthchecks: [ healthchecks: [
{ {
key: 'common.openssl',
label: 'OpenSSL Installed', label: 'OpenSSL Installed',
run: async (_: EnvironmentInfo) => { run: async (_: EnvironmentInfo) => {
const isAvailable = await commandSucceeds('openssl version'); const isAvailable = await commandSucceeds('openssl version');
@@ -72,6 +75,7 @@ export function getHealthchecks(): Healthchecks {
}, },
}, },
{ {
key: 'common.watchman',
label: 'Watchman Installed', label: 'Watchman Installed',
run: async (_: EnvironmentInfo) => { run: async (_: EnvironmentInfo) => {
const isAvailable = await isWatchmanAvailable(); const isAvailable = await isWatchmanAvailable();
@@ -88,6 +92,7 @@ export function getHealthchecks(): Healthchecks {
isSkipped: false, isSkipped: false,
healthchecks: [ healthchecks: [
{ {
key: 'android.sdk',
label: 'SDK Installed', label: 'SDK Installed',
isRequired: true, isRequired: true,
run: async (e: EnvironmentInfo) => ({ run: async (e: EnvironmentInfo) => ({
@@ -104,6 +109,7 @@ export function getHealthchecks(): Healthchecks {
isSkipped: false, isSkipped: false,
healthchecks: [ healthchecks: [
{ {
key: 'ios.sdk',
label: 'SDK Installed', label: 'SDK Installed',
isRequired: true, isRequired: true,
run: async (e: EnvironmentInfo) => ({ run: async (e: EnvironmentInfo) => ({
@@ -111,6 +117,7 @@ export function getHealthchecks(): Healthchecks {
}), }),
}, },
{ {
key: 'ios.xcode',
label: 'XCode Installed', label: 'XCode Installed',
isRequired: true, isRequired: true,
run: async (e: EnvironmentInfo) => ({ run: async (e: EnvironmentInfo) => ({
@@ -118,6 +125,7 @@ export function getHealthchecks(): Healthchecks {
}), }),
}, },
{ {
key: 'ios.xcode-select',
label: 'xcode-select set', label: 'xcode-select set',
isRequired: true, isRequired: true,
run: async (_: EnvironmentInfo) => ({ run: async (_: EnvironmentInfo) => ({
@@ -125,6 +133,7 @@ export function getHealthchecks(): Healthchecks {
}), }),
}, },
{ {
key: 'ios.instruments',
label: 'Instruments exists', label: 'Instruments exists',
isRequired: true, isRequired: true,
run: async (_: EnvironmentInfo) => { run: async (_: EnvironmentInfo) => {
@@ -164,17 +173,20 @@ export async function runHealthchecks(): Promise<
{ {
label: category.label, label: category.label,
results: await Promise.all( results: await Promise.all(
category.healthchecks.map(async ({label, run, isRequired}) => ({ category.healthchecks.map(
label, async ({key, label, run, isRequired}) => ({
isRequired: isRequired ?? true, key,
result: await run(environmentInfo).catch(e => { label,
console.error(e); isRequired: isRequired ?? true,
// TODO Improve result type to be: OK | Problem(message, fix...) result: await run(environmentInfo).catch(e => {
return { console.error(e);
hasProblem: true, // TODO Improve result type to be: OK | Problem(message, fix...)
}; return {
hasProblem: true,
};
}),
}), }),
})), ),
), ),
}, },
]; ];

View File

@@ -23,14 +23,14 @@ import runHealthchecks, {
HealthcheckEventsHandler, HealthcheckEventsHandler,
} from '../utils/runHealthchecks'; } from '../utils/runHealthchecks';
import { import {
HealthcheckResult,
updateHealthcheckResult, updateHealthcheckResult,
startHealthchecks, startHealthchecks,
finishHealthchecks, finishHealthchecks,
HealthcheckStatus,
} from '../reducers/healthchecks'; } from '../reducers/healthchecks';
type StateFromProps = { type StateFromProps = {
healthcheckStatus: HealthcheckStatus; healthcheckResult: HealthcheckResult;
} & HealthcheckSettings; } & HealthcheckSettings;
type DispatchFromProps = { type DispatchFromProps = {
@@ -52,7 +52,11 @@ class DoctorBar extends Component<Props, State> {
} }
async showMessageIfChecksFailed() { async showMessageIfChecksFailed() {
await runHealthchecks(this.props); await runHealthchecks(this.props);
if (this.props.healthcheckStatus === 'FAILED') { if (
(this.props.healthcheckResult.status === 'FAILED' ||
this.props.healthcheckResult.status === 'WARNING') &&
!this.props.healthcheckResult.isAcknowledged
) {
this.setVisible(true); this.setVisible(true);
} }
} }
@@ -99,11 +103,11 @@ export default connect<StateFromProps, DispatchFromProps, {}, Store>(
({ ({
settingsState: {enableAndroid}, settingsState: {enableAndroid},
healthchecks: { healthchecks: {
healthcheckReport: {status}, healthcheckReport: {result},
}, },
}) => ({ }) => ({
enableAndroid, enableAndroid,
healthcheckStatus: status, healthcheckResult: result,
}), }),
{ {
setActiveSheet, setActiveSheet,

View File

@@ -26,14 +26,12 @@ import {State as Store} from '../reducers';
import { import {
HealthcheckResult, HealthcheckResult,
HealthcheckReportCategory, HealthcheckReportCategory,
HealthcheckReportItem,
HealthcheckReport, HealthcheckReport,
startHealthchecks, startHealthchecks,
finishHealthchecks, finishHealthchecks,
updateHealthcheckResult, updateHealthcheckResult,
acknowledgeProblems, acknowledgeProblems,
resetAcknowledgedProblems, resetAcknowledgedProblems,
HealthcheckStatus,
} from '../reducers/healthchecks'; } from '../reducers/healthchecks';
import runHealthchecks, { import runHealthchecks, {
HealthcheckSettings, HealthcheckSettings,
@@ -123,17 +121,18 @@ function CenteredCheckbox(props: {
); );
} }
function HealthcheckIcon(props: {check: HealthcheckResult}) { function HealthcheckIcon(props: {checkResult: HealthcheckResult}) {
switch (props.check.status) { const {checkResult: check} = props;
switch (props.checkResult.status) {
case 'IN_PROGRESS': case 'IN_PROGRESS':
return <LoadingIndicator size={16} title={props.check.message} />; return <LoadingIndicator size={16} title={props.checkResult.message} />;
case 'SKIPPED': case 'SKIPPED':
return ( return (
<Glyph <Glyph
size={16} size={16}
name={'question'} name={'question'}
color={colors.grey} color={colors.grey}
title={props.check.message} title={props.checkResult.message}
/> />
); );
case 'SUCCESS': case 'SUCCESS':
@@ -142,7 +141,7 @@ function HealthcheckIcon(props: {check: HealthcheckResult}) {
size={16} size={16}
name={'checkmark'} name={'checkmark'}
color={colors.green} color={colors.green}
title={props.check.message} title={props.checkResult.message}
/> />
); );
case 'FAILED': case 'FAILED':
@@ -151,17 +150,8 @@ function HealthcheckIcon(props: {check: HealthcheckResult}) {
size={16} size={16}
name={'cross'} name={'cross'}
color={colors.red} color={colors.red}
title={props.check.message} title={props.checkResult.message}
/> variant={check.isAcknowledged ? 'outline' : 'filled'}
);
case 'FAILED_ACKNOWLEDGED':
return (
<Glyph
size={16}
name={'cross'}
color={colors.red}
title={props.check.message}
variant="outline"
/> />
); );
default: default:
@@ -170,26 +160,26 @@ function HealthcheckIcon(props: {check: HealthcheckResult}) {
size={16} size={16}
name={'caution'} name={'caution'}
color={colors.yellow} color={colors.yellow}
title={props.check.message} title={props.checkResult.message}
/> />
); );
} }
} }
function HealthcheckDisplay(props: { function HealthcheckDisplay(props: {
category: HealthcheckReportCategory; label: string;
check: HealthcheckReportItem; result: HealthcheckResult;
onClick?: () => void; onClick?: () => void;
}) { }) {
return ( return (
<FlexColumn shrink> <FlexColumn shrink>
<HealthcheckDisplayContainer shrink title={props.check.message}> <HealthcheckDisplayContainer shrink title={props.result.message}>
<HealthcheckIcon check={props.check} /> <HealthcheckIcon checkResult={props.result} />
<HealthcheckLabel <HealthcheckLabel
underline={!!props.onClick} underline={!!props.onClick}
cursor={props.onClick && 'pointer'} cursor={props.onClick && 'pointer'}
onClick={props.onClick}> onClick={props.onClick}>
{props.check.label} {props.label}
</HealthcheckLabel> </HealthcheckLabel>
</HealthcheckDisplayContainer> </HealthcheckDisplayContainer>
</FlexColumn> </FlexColumn>
@@ -221,20 +211,13 @@ function SideMessageDisplay(props: {
} }
} }
function hasProblems(status: HealthcheckStatus) { function hasProblems(result: HealthcheckResult) {
return ( const {status} = result;
status === 'FAILED' || return status === 'FAILED' || status === 'WARNING';
status === 'FAILED_ACKNOWLEDGED' ||
status === 'WARNING'
);
} }
function hasFailedChecks(status: HealthcheckStatus) { function hasNewProblems(result: HealthcheckResult) {
return status === 'FAILED' || status === 'FAILED_ACKNOWLEDGED'; return hasProblems(result) && !result.isAcknowledged;
}
function hasNewFailedChecks(status: HealthcheckStatus) {
return status === 'FAILED';
} }
export type State = { export type State = {
@@ -254,21 +237,21 @@ class DoctorSheet extends Component<Props, State> {
static getDerivedStateFromProps(props: Props, state: State): State | null { static getDerivedStateFromProps(props: Props, state: State): State | null {
if ( if (
!state.acknowledgeCheckboxVisible && !state.acknowledgeCheckboxVisible &&
hasFailedChecks(props.healthcheckReport.status) hasProblems(props.healthcheckReport.result)
) { ) {
return { return {
...state, ...state,
acknowledgeCheckboxVisible: true, acknowledgeCheckboxVisible: true,
acknowledgeOnClose: acknowledgeOnClose:
state.acknowledgeOnClose === undefined state.acknowledgeOnClose === undefined
? !hasNewFailedChecks(props.healthcheckReport.status) ? !hasNewProblems(props.healthcheckReport.result)
: state.acknowledgeOnClose, : state.acknowledgeOnClose,
}; };
} }
if ( if (
state.acknowledgeCheckboxVisible && state.acknowledgeCheckboxVisible &&
!hasFailedChecks(props.healthcheckReport.status) !hasProblems(props.healthcheckReport.result)
) { ) {
return { return {
...state, ...state,
@@ -296,8 +279,8 @@ class DoctorSheet extends Component<Props, State> {
}); });
} }
openHelpUrl(check: HealthcheckReportItem): void { openHelpUrl(helpUrl?: string): void {
check.helpUrl && shell.openExternal(check.helpUrl); helpUrl && shell.openExternal(helpUrl);
} }
async runHealthchecks() { async runHealthchecks() {
@@ -311,29 +294,34 @@ class DoctorSheet extends Component<Props, State> {
<FlexRow> <FlexRow>
<HealthcheckListContainer> <HealthcheckListContainer>
{Object.values(this.props.healthcheckReport.categories).map( {Object.values(this.props.healthcheckReport.categories).map(
(category: HealthcheckReportCategory, categoryIdx: number) => { (category: HealthcheckReportCategory) => {
return ( return (
<CategoryContainer key={categoryIdx}> <CategoryContainer key={category.key}>
<HealthcheckDisplay check={category} category={category} /> <HealthcheckDisplay
{category.status !== 'SKIPPED' && ( label={category.label}
result={category.result}
/>
{category.result.status !== 'SKIPPED' && (
<CategoryContainer> <CategoryContainer>
{category.checks.map((check, checkIdx) => ( {Object.values(category.checks).map(check => (
<HealthcheckDisplay <HealthcheckDisplay
key={checkIdx} key={check.key}
category={category} label={check.label}
check={check} result={check.result}
onClick={ onClick={
check.helpUrl check.result.helpUrl
? () => this.openHelpUrl(check) ? () => this.openHelpUrl(check.result.helpUrl)
: undefined : undefined
} }
/> />
))} ))}
</CategoryContainer> </CategoryContainer>
)} )}
{category.status === 'SKIPPED' && ( {category.result.status === 'SKIPPED' && (
<CategoryContainer> <CategoryContainer>
<SkipReasonLabel>{category.message}</SkipReasonLabel> <SkipReasonLabel>
{category.result.message}
</SkipReasonLabel>
</CategoryContainer> </CategoryContainer>
)} )}
</CategoryContainer> </CategoryContainer>
@@ -345,9 +333,9 @@ class DoctorSheet extends Component<Props, State> {
<SideContainer shrink> <SideContainer shrink>
<SideMessageDisplay <SideMessageDisplay
isHealthcheckInProgress={ isHealthcheckInProgress={
this.props.healthcheckReport.status === 'IN_PROGRESS' this.props.healthcheckReport.result.status === 'IN_PROGRESS'
} }
hasProblems={hasProblems(this.props.healthcheckReport.status)} hasProblems={hasProblems(this.props.healthcheckReport.result)}
/> />
</SideContainer> </SideContainer>
</FlexRow> </FlexRow>
@@ -366,7 +354,9 @@ class DoctorSheet extends Component<Props, State> {
Close Close
</Button> </Button>
<Button <Button
disabled={this.props.healthcheckReport.status === 'IN_PROGRESS'} disabled={
this.props.healthcheckReport.result.status === 'IN_PROGRESS'
}
type="primary" type="primary"
compact compact
padded padded

View File

@@ -0,0 +1,335 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`acknowledgeProblems 1`] = `
Object {
"acknowledgedProblems": Array [
"ios.sdk",
"common.openssl",
],
"healthcheckReport": Object {
"categories": Object {
"android": Object {
"checks": Object {
"android.sdk": Object {
"key": "android.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": true,
"status": "SUCCESS",
},
},
},
"key": "android",
"label": "Android",
"result": Object {
"isAcknowledged": true,
"status": "SUCCESS",
},
},
"common": Object {
"checks": Object {
"common.openssl": Object {
"key": "common.openssl",
"label": "OpenSSL Istalled",
"result": Object {
"isAcknowledged": true,
"status": "FAILED",
},
},
},
"key": "common",
"label": "Common",
"result": Object {
"isAcknowledged": true,
"status": "FAILED",
},
},
"ios": Object {
"checks": Object {
"ios.sdk": Object {
"key": "ios.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": true,
"status": "FAILED",
},
},
},
"key": "ios",
"label": "iOS",
"result": Object {
"isAcknowledged": true,
"status": "FAILED",
},
},
},
"result": Object {
"isAcknowledged": true,
"status": "FAILED",
},
},
}
`;
exports[`finish 1`] = `
Object {
"acknowledgedProblems": Array [],
"healthcheckReport": Object {
"categories": Object {
"android": Object {
"checks": Object {
"android.sdk": Object {
"key": "android.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "android",
"label": "Android",
"result": Object {
"isAcknowledged": false,
"status": "SUCCESS",
},
},
"common": Object {
"checks": Object {
"common.openssl": Object {
"key": "common.openssl",
"label": "OpenSSL Istalled",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "common",
"label": "Common",
"result": Object {
"isAcknowledged": false,
"status": "SUCCESS",
},
},
"ios": Object {
"checks": Object {
"ios.sdk": Object {
"key": "ios.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "ios",
"label": "iOS",
"result": Object {
"isAcknowledged": false,
"status": "SUCCESS",
},
},
},
"result": Object {
"status": "SUCCESS",
},
},
}
`;
exports[`startHealthCheck 1`] = `
Object {
"acknowledgedProblems": Array [],
"healthcheckReport": Object {
"categories": Object {
"android": Object {
"checks": Object {
"android.sdk": Object {
"key": "android.sdk",
"label": "SDK Installed",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"key": "android",
"label": "Android",
"result": Object {
"status": "IN_PROGRESS",
},
},
"common": Object {
"checks": Object {
"common.openssl": Object {
"key": "common.openssl",
"label": "OpenSSL Istalled",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"key": "common",
"label": "Common",
"result": Object {
"status": "IN_PROGRESS",
},
},
"ios": Object {
"checks": Object {
"ios.sdk": Object {
"key": "ios.sdk",
"label": "SDK Installed",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"key": "ios",
"label": "iOS",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"result": Object {
"status": "IN_PROGRESS",
},
},
}
`;
exports[`statuses updated after healthchecks finished 1`] = `
Object {
"acknowledgedProblems": Array [],
"healthcheckReport": Object {
"categories": Object {
"android": Object {
"checks": Object {
"android.sdk": Object {
"key": "android.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "FAILED",
},
},
},
"key": "android",
"label": "Android",
"result": Object {
"isAcknowledged": false,
"status": "FAILED",
},
},
"common": Object {
"checks": Object {
"common.openssl": Object {
"key": "common.openssl",
"label": "OpenSSL Istalled",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "common",
"label": "Common",
"result": Object {
"status": "SUCCESS",
},
},
"ios": Object {
"checks": Object {
"ios.sdk": Object {
"key": "ios.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "ios",
"label": "iOS",
"result": Object {
"status": "SUCCESS",
},
},
},
"result": Object {
"isAcknowledged": false,
"status": "FAILED",
},
},
}
`;
exports[`updateHealthcheckResult 1`] = `
Object {
"acknowledgedProblems": Array [],
"healthcheckReport": Object {
"categories": Object {
"android": Object {
"checks": Object {
"android.sdk": Object {
"key": "android.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "android",
"label": "Android",
"result": Object {
"status": "IN_PROGRESS",
},
},
"common": Object {
"checks": Object {
"common.openssl": Object {
"key": "common.openssl",
"label": "OpenSSL Istalled",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"key": "common",
"label": "Common",
"result": Object {
"status": "IN_PROGRESS",
},
},
"ios": Object {
"checks": Object {
"ios.sdk": Object {
"key": "ios.sdk",
"label": "SDK Installed",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"key": "ios",
"label": "iOS",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"result": Object {
"status": "IN_PROGRESS",
},
},
}
`;

View File

@@ -64,143 +64,106 @@ const HEALTHCHECKS: Healthchecks = {
test('startHealthCheck', () => { test('startHealthCheck', () => {
const res = reducer(undefined, startHealthchecks(HEALTHCHECKS)); const res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
expect(res.healthcheckReport.status).toBe('IN_PROGRESS'); expect(res).toMatchSnapshot();
expect(res.healthcheckReport.categories.length).toBe(3);
expect(res.healthcheckReport.categories[0].status).toEqual('IN_PROGRESS');
expect(res.healthcheckReport.categories[0].label).toEqual('iOS');
expect(res.healthcheckReport.categories[0].checks.length).toEqual(1);
expect(res.healthcheckReport.categories[0].checks[0].label).toEqual(
'SDK Installed',
);
expect(res.healthcheckReport.categories[0].checks[0].status).toEqual(
'IN_PROGRESS',
);
}); });
test('updateHealthcheckResult', () => { test('updateHealthcheckResult', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS)); let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer( res = reducer(
res, res,
updateHealthcheckResult(0, 0, { updateHealthcheckResult('android', 'android.sdk', {
message: 'Updated Test Message', message: 'Updated Test Message',
isAcknowledged: false,
status: 'SUCCESS', status: 'SUCCESS',
}), }),
); );
expect(res.healthcheckReport.status).toBe('IN_PROGRESS'); expect(res).toMatchSnapshot();
expect(res.healthcheckReport.categories[0].checks[0].message).toEqual(
'Updated Test Message',
);
expect(res.healthcheckReport.categories[0].checks[0].status).toEqual(
'SUCCESS',
);
expect(res.healthcheckReport.categories[0].status).toEqual('IN_PROGRESS');
expect(res.healthcheckReport.categories[1].checks[0].message).toBeUndefined();
expect(res.healthcheckReport.categories[1].checks[0].status).toEqual(
'IN_PROGRESS',
);
expect(res.healthcheckReport.categories[1].status).toEqual('IN_PROGRESS');
}); });
test('finish', () => { test('finish', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS)); let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer( res = reducer(
res, res,
updateHealthcheckResult(0, 0, { updateHealthcheckResult('ios', 'ios.sdk', {
message: 'Updated Test Message', message: 'Updated Test Message',
isAcknowledged: false,
status: 'SUCCESS', status: 'SUCCESS',
}), }),
); );
res = reducer( res = reducer(
res, res,
updateHealthcheckResult(1, 0, { updateHealthcheckResult('android', 'android.sdk', {
message: 'Updated Test Message', message: 'Updated Test Message',
isAcknowledged: false,
status: 'SUCCESS', status: 'SUCCESS',
}), }),
); );
res = reducer( res = reducer(
res, res,
updateHealthcheckResult(2, 0, { updateHealthcheckResult('common', 'common.openssl', {
message: 'Updated Test Message', message: 'Updated Test Message',
isAcknowledged: false,
status: 'SUCCESS', status: 'SUCCESS',
}), }),
); );
res = reducer(res, finishHealthchecks()); res = reducer(res, finishHealthchecks());
expect(res.healthcheckReport.status).toBe('SUCCESS'); expect(res).toMatchSnapshot();
expect(res.healthcheckReport.categories.map(c => c.status)).toEqual([
'SUCCESS',
'SUCCESS',
'SUCCESS',
]);
}); });
test('statuses updated after healthchecks finished', () => { test('statuses updated after healthchecks finished', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS)); let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer( res = reducer(
res, res,
updateHealthcheckResult(1, 0, { updateHealthcheckResult('android', 'android.sdk', {
message: 'Updated Test Message', message: 'Updated Test Message',
isAcknowledged: false,
status: 'FAILED', status: 'FAILED',
}), }),
); );
res = reducer( res = reducer(
res, res,
updateHealthcheckResult(0, 0, { updateHealthcheckResult('ios', 'ios.sdk', {
message: 'Updated Test Message', message: 'Updated Test Message',
isAcknowledged: false,
status: 'SUCCESS', status: 'SUCCESS',
}), }),
); );
res = reducer( res = reducer(
res, res,
updateHealthcheckResult(2, 0, { updateHealthcheckResult('common', 'common.openssl', {
message: 'Updated Test Message', message: 'Updated Test Message',
isAcknowledged: false,
status: 'SUCCESS', status: 'SUCCESS',
}), }),
); );
res = reducer(res, finishHealthchecks()); res = reducer(res, finishHealthchecks());
expect(res.healthcheckReport.status).toBe('FAILED'); expect(res).toMatchSnapshot();
expect(res.healthcheckReport.categories.map(c => c.status)).toEqual([
'SUCCESS',
'FAILED',
'SUCCESS',
]);
expect(res.healthcheckReport.categories[1].checks[0].message).toEqual(
'Updated Test Message',
);
expect(res.healthcheckReport.categories[1].checks[0].status).toEqual(
'FAILED',
);
}); });
test('acknowledgeProblems', () => { test('acknowledgeProblems', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS)); let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer( res = reducer(
res, res,
updateHealthcheckResult(0, 0, { updateHealthcheckResult('ios', 'ios.sdk', {
isAcknowledged: false,
status: 'FAILED', status: 'FAILED',
}), }),
); );
res = reducer( res = reducer(
res, res,
updateHealthcheckResult(1, 0, { updateHealthcheckResult('android', 'android.sdk', {
isAcknowledged: false,
status: 'SUCCESS', status: 'SUCCESS',
}), }),
); );
res = reducer( res = reducer(
res, res,
updateHealthcheckResult(2, 0, { updateHealthcheckResult('common', 'common.openssl', {
isAcknowledged: false,
status: 'FAILED', status: 'FAILED',
}), }),
); );
res = reducer(res, finishHealthchecks()); res = reducer(res, finishHealthchecks());
res = reducer(res, acknowledgeProblems()); res = reducer(res, acknowledgeProblems());
expect(res.healthcheckReport.categories[0].status).toEqual( expect(res).toMatchSnapshot();
'FAILED_ACKNOWLEDGED',
);
expect(res.healthcheckReport.categories[0].checks[0].status).toEqual(
'FAILED_ACKNOWLEDGED',
);
expect(res.healthcheckReport.categories[1].status).toEqual('SUCCESS');
expect(res.healthcheckReport.categories[2].status).toEqual(
'FAILED_ACKNOWLEDGED',
);
}); });

View File

@@ -27,8 +27,8 @@ export type Action =
| { | {
type: 'UPDATE_HEALTHCHECK_RESULT'; type: 'UPDATE_HEALTHCHECK_RESULT';
payload: { payload: {
categoryIdx: number; categoryKey: string;
itemIdx: number; itemKey: string;
result: HealthcheckResult; result: HealthcheckResult;
}; };
} }
@@ -41,157 +41,170 @@ export type Action =
const INITIAL_STATE: State = { const INITIAL_STATE: State = {
healthcheckReport: { healthcheckReport: {
status: 'IN_PROGRESS', result: {status: 'IN_PROGRESS'},
categories: [], categories: {},
}, },
acknowledgedProblems: [], acknowledgedProblems: [],
}; };
type Dictionary<T> = {[key: string]: T};
export type HealthcheckStatus = export type HealthcheckStatus =
| 'IN_PROGRESS' | 'IN_PROGRESS'
| 'SUCCESS' | 'SUCCESS'
| 'FAILED' | 'FAILED'
| 'FAILED_ACKNOWLEDGED'
| 'SKIPPED' | 'SKIPPED'
| 'WARNING'; | 'WARNING';
export type HealthcheckResult = { export type HealthcheckResult = {
status: HealthcheckStatus; status: HealthcheckStatus;
isAcknowledged?: boolean;
message?: string; message?: string;
helpUrl?: string; helpUrl?: string;
}; };
export type HealthcheckReportItem = { export type HealthcheckReportItem = {
key: string;
label: string; label: string;
} & HealthcheckResult; result: HealthcheckResult;
export type HealthcheckReportCategory = {
label: string;
checks: Array<HealthcheckReportItem>;
} & HealthcheckResult;
export type HealthcheckReport = {
status: HealthcheckStatus;
categories: Array<HealthcheckReportCategory>;
}; };
function getHealthcheckIdentifier( export type HealthcheckReportCategory = {
category: HealthcheckReportCategory, key: string;
item: HealthcheckReportItem, label: string;
) { result: HealthcheckResult;
return `${category.label} : ${item.label}`; checks: Dictionary<HealthcheckReportItem>;
} };
export type HealthcheckReport = {
result: HealthcheckResult;
categories: Dictionary<HealthcheckReportCategory>;
};
function recomputeHealthcheckStatus(draft: State): void { function recomputeHealthcheckStatus(draft: State): void {
draft.healthcheckReport.status = computeAggregatedStatus( draft.healthcheckReport.result = computeAggregatedResult(
draft.healthcheckReport.categories.map(c => c.status), Object.values(draft.healthcheckReport.categories).map(c => c.result),
); );
} }
function computeAggregatedStatus( function computeAggregatedResult(
statuses: HealthcheckStatus[], results: HealthcheckResult[],
): HealthcheckStatus { ): HealthcheckResult {
return statuses.some(s => s === 'IN_PROGRESS') return results.some(r => r.status === 'IN_PROGRESS')
? 'IN_PROGRESS' ? {status: 'IN_PROGRESS'}
: statuses.every(s => s === 'SUCCESS') : results.every(r => r.status === 'SUCCESS')
? 'SUCCESS' ? {status: 'SUCCESS'}
: statuses.some(s => s === 'FAILED') : results.some(r => r.status === 'FAILED' && !r.isAcknowledged)
? 'FAILED' ? {status: 'FAILED', isAcknowledged: false}
: statuses.some(s => s === 'FAILED_ACKNOWLEDGED') : results.some(r => r.status === 'FAILED')
? 'FAILED_ACKNOWLEDGED' ? {status: 'FAILED', isAcknowledged: true}
: statuses.some(s => s === 'WARNING') : results.some(r => r.status === 'WARNING' && !r.isAcknowledged)
? 'WARNING' ? {status: 'WARNING', isAcknowledged: false}
: 'SKIPPED'; : results.some(r => r.status === 'WARNING')
? {status: 'WARNING', isAcknowledged: true}
: {status: 'SKIPPED'};
} }
const updateCheckResult = produce( const updateCheckResult = produce(
( (
draft: State, draft: State,
{ {
categoryIdx, categoryKey,
itemIdx, itemKey,
result, result,
}: { }: {
categoryIdx: number; categoryKey: string;
itemIdx: number; itemKey: string;
result: HealthcheckResult; result: HealthcheckResult;
}, },
) => { ) => {
const category = draft.healthcheckReport.categories[categoryIdx]; const category = draft.healthcheckReport.categories[categoryKey];
const item = category.checks[itemIdx]; const item = category.checks[itemKey];
Object.assign(item, result); Object.assign(item.result, result);
if ( item.result.isAcknowledged = draft.acknowledgedProblems.includes(item.key);
result.status === 'FAILED' &&
draft.acknowledgedProblems.includes(
getHealthcheckIdentifier(category, item),
)
) {
item.status = 'FAILED_ACKNOWLEDGED';
}
}, },
); );
function createDict<T>(pairs: [string, T][]): Dictionary<T> {
const obj: Dictionary<T> = {};
for (const pair of pairs) {
obj[pair[0]] = pair[1];
}
return obj;
}
const start = produce((draft: State, healthchecks: Healthchecks) => { const start = produce((draft: State, healthchecks: Healthchecks) => {
draft.healthcheckReport = { draft.healthcheckReport = {
status: 'IN_PROGRESS', result: {status: 'IN_PROGRESS'},
categories: Object.values(healthchecks) categories: createDict<HealthcheckReportCategory>(
.map(category => { Object.entries(healthchecks).map(([categoryKey, category]) => {
if (category.isSkipped) { if (category.isSkipped) {
return { return [
status: 'SKIPPED', categoryKey,
label: category.label, {
checks: [], key: categoryKey,
message: category.skipReason, result: {
}; status: 'SKIPPED',
message: category.skipReason,
},
label: category.label,
checks: createDict<HealthcheckReportItem>([]),
},
];
} }
return { return [
status: 'IN_PROGRESS', categoryKey,
label: category.label, {
checks: category.healthchecks.map(x => ({ key: categoryKey,
status: 'IN_PROGRESS', result: {status: 'IN_PROGRESS'},
label: x.label, label: category.label,
})), checks: createDict<HealthcheckReportItem>(
}; category.healthchecks.map(check => [
}) check.key,
.filter(x => !!x) {
.map(x => x as HealthcheckReportCategory), key: check.key,
result: {status: 'IN_PROGRESS'},
label: check.label,
},
]),
),
},
];
}),
),
}; };
}); });
const finish = produce((draft: State) => { const finish = produce((draft: State) => {
draft.healthcheckReport.categories Object.values(draft.healthcheckReport.categories)
.filter(cat => cat.status !== 'SKIPPED') .filter(cat => cat.result.status !== 'SKIPPED')
.forEach(cat => { .forEach(cat => {
cat.message = undefined; cat.result.message = undefined;
cat.status = computeAggregatedStatus(cat.checks.map(c => c.status)); cat.result = computeAggregatedResult(
Object.values(cat.checks).map(c => c.result),
);
}); });
recomputeHealthcheckStatus(draft); recomputeHealthcheckStatus(draft);
if (draft.healthcheckReport.status === 'SUCCESS') { if (draft.healthcheckReport.result.status === 'SUCCESS') {
setAcknowledgedProblemsToEmpty(draft); setAcknowledgedProblemsToEmpty(draft);
} }
}); });
const acknowledge = produce((draft: State) => { const acknowledge = produce((draft: State) => {
draft.acknowledgedProblems = ([] as string[]).concat( draft.acknowledgedProblems = ([] as string[]).concat(
...draft.healthcheckReport.categories.map(cat => ...Object.values(draft.healthcheckReport.categories).map(cat =>
cat.checks Object.values(cat.checks)
.filter( .filter(
chk => chk =>
chk.status === 'FAILED' || chk.result.status === 'FAILED' || chk.result.status === 'WARNING',
chk.status === 'FAILED_ACKNOWLEDGED' ||
chk.status === 'WARNING',
) )
.map(chk => getHealthcheckIdentifier(cat, chk)), .map(chk => chk.key),
), ),
); );
draft.healthcheckReport.categories.forEach(cat => { Object.values(draft.healthcheckReport.categories).forEach(cat => {
if (cat.status === 'FAILED') { cat.result.isAcknowledged = true;
cat.status = 'FAILED_ACKNOWLEDGED'; Object.values(cat.checks).forEach(chk => {
} chk.result.isAcknowledged = true;
cat.checks.forEach(chk => {
if (chk.status == 'FAILED') {
chk.status = 'FAILED_ACKNOWLEDGED';
}
}); });
}); });
recomputeHealthcheckStatus(draft); recomputeHealthcheckStatus(draft);
@@ -199,14 +212,10 @@ const acknowledge = produce((draft: State) => {
function setAcknowledgedProblemsToEmpty(draft: State) { function setAcknowledgedProblemsToEmpty(draft: State) {
draft.acknowledgedProblems = []; draft.acknowledgedProblems = [];
draft.healthcheckReport.categories.forEach(cat => { Object.values(draft.healthcheckReport.categories).forEach(cat => {
if (cat.status === 'FAILED_ACKNOWLEDGED') { cat.result.isAcknowledged = false;
cat.status = 'FAILED'; Object.values(cat.checks).forEach(chk => {
} chk.result.isAcknowledged = false;
cat.checks.forEach(chk => {
if (chk.status == 'FAILED_ACKNOWLEDGED') {
chk.status = 'FAILED';
}
}); });
}); });
} }
@@ -234,14 +243,14 @@ export default function reducer(
} }
export const updateHealthcheckResult = ( export const updateHealthcheckResult = (
categoryIdx: number, categoryKey: string,
itemIdx: number, itemKey: string,
result: HealthcheckResult, result: HealthcheckResult,
): Action => ({ ): Action => ({
type: 'UPDATE_HEALTHCHECK_RESULT', type: 'UPDATE_HEALTHCHECK_RESULT',
payload: { payload: {
categoryIdx, categoryKey,
itemIdx, itemKey,
result, result,
}, },
}); });

View File

@@ -15,8 +15,8 @@ let runningHealthcheck: Promise<void>;
export type HealthcheckEventsHandler = { export type HealthcheckEventsHandler = {
updateHealthcheckResult: ( updateHealthcheckResult: (
categoryIdx: number, categoryKey: string,
itemIdx: number, itemKey: string,
result: HealthcheckResult, result: HealthcheckResult,
) => void; ) => void;
startHealthchecks: (healthchecks: Healthchecks) => void; startHealthchecks: (healthchecks: Healthchecks) => void;
@@ -41,17 +41,11 @@ async function launchHealthchecks(options: HealthcheckOptions): Promise<void> {
} }
options.startHealthchecks(healthchecks); options.startHealthchecks(healthchecks);
const environmentInfo = await getEnvInfo(); const environmentInfo = await getEnvInfo();
const categories = Object.values(healthchecks); for (const [categoryKey, category] of Object.entries(healthchecks)) {
for (const [categoryIdx, category] of categories.entries()) {
if (category.isSkipped) { if (category.isSkipped) {
continue; continue;
} }
for ( for (const h of category.healthchecks) {
let healthcheckIdx = 0;
healthcheckIdx < category.healthchecks.length;
healthcheckIdx++
) {
const h = category.healthchecks[healthcheckIdx];
const checkResult = await h.run(environmentInfo); const checkResult = await h.run(environmentInfo);
const result: HealthcheckResult = const result: HealthcheckResult =
checkResult.hasProblem && h.isRequired checkResult.hasProblem && h.isRequired
@@ -65,7 +59,7 @@ async function launchHealthchecks(options: HealthcheckOptions): Promise<void> {
helpUrl: checkResult.helpUrl, helpUrl: checkResult.helpUrl,
} }
: {status: 'SUCCESS'}; : {status: 'SUCCESS'};
options.updateHealthcheckResult(categoryIdx, healthcheckIdx, result); options.updateHealthcheckResult(categoryKey, h.key, result);
} }
} }
options.finishHealthchecks(); options.finishHealthchecks();

View File

@@ -294,5 +294,8 @@
], ],
"play": [ "play": [
16 16
],
"cross-outline": [
16
] ]
} }