Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

patch: basic E2E tests for macOS #4216

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
31 changes: 30 additions & 1 deletion .github/actions/test/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,20 @@ runs:
with:
python-version: '3.11'

- name: Setup Virtual Drive on MacOS
if: runner.os == 'macOS'
shell: bash
run: |
hdiutil create -size 4096m -layout NONE -o virtual_test_disk.dmg
virtual_path=$(hdiutil attach -nomount virtual_test_disk.dmg | awk '{print $1}')
echo "TARGET_DRIVE=${virtual_path}" >> $GITHUB_ENV
echo "ETCHER_INCLUDE_VIRTUAL_DRIVES=1" >> $GITHUB_ENV

- name: Test release
shell: bash
run: |
# Build and Test release

## FIXME: causes issues with `xxhash` which tries to load a debug build which doens't exist and cannot be compiled
# if [[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]]; then
# export DEBUG='electron-forge:*,sidecar'
Expand All @@ -63,11 +74,29 @@ runs:

npm run lint
npm run package
npm run wdio # test stage, note that it requires the package to be done first

# tests requires the app to already be built

# # only run e2e tests on Mac as it's the only supported platform atm
if [[ '${{ runner.os }}' == 'macOS' ]]; then
# run all tests on macOS including E2E
# E2E tests can't input the administrative password, therefore the tests need to run as root
wget -q -O ${{ env.TEST_SOURCE_FILE }} ${{ env.TEST_SOURCE_URL }}
sudo \
TARGET_DRIVE=${{ env.TARGET_DRIVE }} \
ETCHER_INCLUDE_VIRTUAL_DRIVES=1 \
TEST_SOURCE_FILE: $(pwd)/${{ env.TEST_SOURCE_FILE }} \
TEST_SOURCE_URL: ${{ env.TEST_SOURCE_URL }} \
npm run wdio:ci
else
npm run wdio:unit
fi

env:
# https://www.electronjs.org/docs/latest/api/environment-variables
ELECTRON_NO_ATTACH_CONSOLE: 'true'
TEST_SOURCE_URL: 'https://api.balena-cloud.com/download?deviceType=raspberrypi4-64&version=5.2.8&fileType=.zip'
TEST_SOURCE_FILE: 'raspberrypi4-64-5.2.8-v16.1.10.img.zip'

- name: Compress custom source
if: runner.os != 'Windows'
Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,10 @@ secrets/WINDOWS_SIGNING.pfx

#local development
.yalc
yalc.lock
yalc.lock

# Test assets
virtual_test_disk.dmg
virtual_test_disk.img
virtual_test_disk.vhd
screenshots/
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run prettify
24 changes: 20 additions & 4 deletions lib/gui/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,18 @@ observe(() => {

function setDrives(drives: Dictionary<DrivelistDrive>) {
// prevent setting drives while flashing otherwise we might lose some while we unmount them
if (!flashState.isFlashing()) {
availableDrives.setDrives(values(drives));
}
availableDrives.setDrives(values(drives));
}

// Spawning the child process without privileges to get the drives list
// TODO: clean up this mess of exports
export let requestMetadata: any;
export let requestMetadata: (params: any) => Promise<SourceMetadata>;
export let startScanner: () => void = () => {
console.log('stopScanner is not yet set');
};
export let stopScanner: () => void = () => {
console.log('stopScanner is not yet set');
};

// start the api and spawn the child process
spawnChildAndConnect({
Expand All @@ -147,6 +151,18 @@ spawnChildAndConnect({
// start scanning
emit('scan', {});

// make startScanner available for the end of flash
startScanner = () => {
console.log('startScanner');
emit('scan', {});
};

// make stopScanner available for the start of flash
stopScanner = () => {
console.log('stopScanner');
emit('scan', {});
};

// make the sourceMetada awaitable to be used on source selection
requestMetadata = async (params: any): Promise<SourceMetadata> => {
emit('sourceMetadata', JSON.stringify(params));
Expand Down
1 change: 1 addition & 0 deletions lib/gui/app/components/drive-selector/drive-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ export class DriveSelector extends React.Component<
primary: !showWarnings,
warning: showWarnings,
disabled: !hasAvailableDrives(),
'data-testid': 'validate-target-button',
}}
{...props}
>
Expand Down
2 changes: 1 addition & 1 deletion lib/gui/app/components/flash-another/flash-another.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface FlashAnotherProps {

export const FlashAnother = (props: FlashAnotherProps) => {
return (
<BaseButton primary onClick={props.onClick}>
<BaseButton primary data-testid="flash-another" onClick={props.onClick}>
{i18next.t('flash.another')}
</BaseButton>
);
Expand Down
2 changes: 1 addition & 1 deletion lib/gui/app/components/flash-results/flash-results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export function FlashResults({
/>
<Txt>{middleEllipsis(image, 24)}</Txt>
</Flex>
<Txt fontSize={24} color="#fff" mb="17px">
<Txt data-testid="flash-results" fontSize={24} color="#fff" mb="17px">
{allFailed
? i18next.t('flash.flashFailed')
: i18next.t('flash.flashCompleted')}
Expand Down
5 changes: 4 additions & 1 deletion lib/gui/app/components/progress-button/progress-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
}}
>
<Flex>
<Txt color="#fff">{status}&nbsp;</Txt>
<Txt data-testid="flash-status" color="#fff">
{status}&nbsp;
</Txt>
<Txt color={colors[type]}>{position}</Txt>
</Flex>
{type && (
Expand All @@ -125,6 +127,7 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
warning={warning}
onClick={this.props.callback}
disabled={this.props.disabled}
data-testid={'flash-now'}
style={{
marginTop: 30,
}}
Expand Down
5 changes: 5 additions & 0 deletions lib/gui/app/components/source-selector/source-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ const URLSelector = ({
cancel={cancel}
primaryButtonProps={{
disabled: loading || !imageURL,
'data-testid': 'source-url-ok',
}}
action={loading ? <Spinner /> : i18next.t('ok')}
done={async () => {
Expand All @@ -186,6 +187,7 @@ const URLSelector = ({
</Txt>
<Input
value={imageURL}
data-testid="source-url-input"
placeholder={i18next.t('source.enterValidURL')}
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
Expand Down Expand Up @@ -638,6 +640,7 @@ export class SourceSelector extends React.Component<
</StepNameButton>
{!flashing && !imageLoading && (
<ChangeButton
data-testid="change-image"
plain
mb={14}
onClick={() => this.reselectSource()}
Expand All @@ -655,6 +658,7 @@ export class SourceSelector extends React.Component<
disabled={this.state.imageSelectorOpen}
primary={this.state.defaultFlowActive}
key="Flash from file"
data-testid="flash-from-file"
flow={{
onClick: () => this.openImageSelector(),
label: i18next.t('source.fromFile'),
Expand All @@ -665,6 +669,7 @@ export class SourceSelector extends React.Component<
/>
<FlowSelector
key="Flash from URL"
data-testid="flash-from-url"
flow={{
onClick: () => this.openURLSelector(),
label: i18next.t('source.fromURL'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
tabIndex={targets.length > 0 ? -1 : 2}
disabled={props.disabled}
onClick={props.openDriveSelector}
data-testid="select-target"
>
{i18next.t('target.selectTarget')}
</StepButton>
Expand Down
2 changes: 0 additions & 2 deletions lib/gui/app/modules/progress-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ export function fromFlashState({
status: string;
position?: string;
} {
console.log(i18next.t('progress.starting'));

if (type === undefined) {
return { status: i18next.t('progress.starting') };
} else if (type === 'decompressing') {
Expand Down
12 changes: 12 additions & 0 deletions lib/gui/app/os/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import * as settings from '../../../gui/app/models/settings';
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
import * as i18next from 'i18next';

// FIXME: this is probably useless now
async function mountSourceDrive() {
// sourceDrivePath is the name of the link in /dev/disk/by-path
const sourceDrivePath = await settings.get('automountOnFileSelect');
Expand All @@ -43,6 +44,17 @@ async function mountSourceDrive() {
*/
export async function selectImage(): Promise<string | undefined> {
await mountSourceDrive();

// For automated E2E testing, we can't set the source file by interacting with the OS dialog,
// so we use an ENV var instead and bypass the dialog. Note that we still need to press the "flash from file" button.
if (
process.env.TEST_SOURCE_FILE !== undefined &&
typeof process.env.TEST_SOURCE_FILE === 'string'
) {
console.log(`test mode: loading ${process.env.TEST_SOURCE_FILE}`);
return process.env.TEST_SOURCE_FILE;
}

const options: electron.OpenDialogOptions = {
// This variable is set when running in GNU/Linux from
// inside an AppImage, and represents the working directory
Expand Down
2 changes: 0 additions & 2 deletions lib/gui/app/pages/main/Flash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,6 @@ async function flashImageToDrive(
errorMessage = messages.error.genericFlashError(error);
}
return errorMessage;
} finally {
availableDrives.setDrives([]);
}

return '';
Expand Down
7 changes: 6 additions & 1 deletion lib/util/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { toJSON } from '../shared/errors';
import { GENERAL_ERROR, SUCCESS } from '../shared/exit-codes';
import type { WriteOptions } from './types/types';
import { write, cleanup } from './child-writer';
import { startScanning } from './scanner';
import { startScanning, stopScanning } from './scanner';
import { getSourceMetadata } from './source-metadata';
import type { DrivelistDrive } from '../shared/drive-constraints';
import type { SourceMetadata } from '../shared/typings/source-selector';
Expand Down Expand Up @@ -222,6 +222,11 @@ function setup(): Promise<EmitLog> {
startScanning();
},

stopScan: () => {
log('Stop scan requested');
stopScanning();
},

// route `cancel` from client
cancel: () => onAbort(GENERAL_ERROR),

Expand Down
2 changes: 2 additions & 0 deletions lib/util/drive-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { geteuid, platform } from 'process';
const adapters: Adapter[] = [
new BlockDeviceAdapter({
includeSystemDrives: () => true,
includeVirtualDrives: () =>
process.env.ETCHER_INCLUDE_VIRTUAL_DRIVES !== 'undefined',
}),
];

Expand Down
7 changes: 5 additions & 2 deletions lib/util/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,15 @@ const COMPUTE_MODULE_DESCRIPTIONS: Dictionary<string> = {
};

const startScanning = () => {
driveScanner.on('attach', (drive) => addDrive(drive));
driveScanner.on('detach', (drive) => removeDrive(drive));
driveScanner.on('attach', addDrive);
driveScanner.on('detach', removeDrive);
driveScanner.start();
};

const stopScanning = () => {
driveScanner.removeListener('attach', addDrive);
driveScanner.removeListener('detach', removeDrive);
availableDrives = [];
driveScanner.stop();
};

Expand Down
Loading
Loading