blob: b6fc3f769cbdf4beeb40e2e4cc05bb1e21e03419 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert' show jsonDecode;
import 'dart:core';
import 'dart:io' show Directory, File, Platform, Process, sleep;
import 'package:test/test.dart';
class ZedmonException implements Exception {
final String message;
ZedmonException(this.message);
@override
String toString() => 'ZedmonException: $message';
}
// Description of the Zedmon device and host-side client.
class ZedmonDescription {
final double shuntResistance;
final int timestampIndex;
final int shuntVoltageIndex;
final int busVoltageIndex;
final int powerIndex;
ZedmonDescription(this.shuntResistance, this.timestampIndex,
this.shuntVoltageIndex, this.busVoltageIndex, this.powerIndex);
}
// Uses `zedmon describe` to create a ZedmonDescription.
Future<ZedmonDescription> getZedmonDescription(String zedmonPath) async {
final result = await Process.run(zedmonPath, ['describe']);
final description = jsonDecode(result.stdout);
final csvFormat = description['csv_header'].split(',');
for (var field in [
'timestamp_micros',
'shunt_voltage',
'bus_voltage',
'power'
]) {
if (!csvFormat.contains(field)) {
throw ZedmonException('CSV header does not contain field "$field".');
}
}
return ZedmonDescription(
description['shunt_resistance'],
csvFormat.indexOf('timestamp_micros'),
csvFormat.indexOf('shunt_voltage'),
csvFormat.indexOf('bus_voltage'),
csvFormat.indexOf('power'));
}
void validateZedmonCsvLine(
String csvLine, ZedmonDescription desc, double powerTolerance) {
final parts = csvLine.split(',');
final shuntVoltage = double.parse(parts[desc.shuntVoltageIndex]);
final busVoltage = double.parse(parts[desc.busVoltageIndex]);
final power = double.parse(parts[desc.powerIndex]);
expect(busVoltage * shuntVoltage / desc.shuntResistance,
closeTo(power, powerTolerance));
}
// Returns the average power measured by `zedmon record --average` for the
// specified number of seconds.
Future<double> measureAveragePower(String zedmonPath, String tempFilePath,
ZedmonDescription desc, int seconds) async {
final result = await Process.run(zedmonPath,
['record', '--out', tempFilePath, '--average', '${seconds}s']);
expect(result.exitCode, equals(0));
final lines = await File(tempFilePath).readAsLines();
expect(lines.length, equals(1));
final parts = lines[0].split(',');
return double.parse(parts[desc.powerIndex]);
}
// In order to run these tests, the host should be connected to exactly one
// Zedmon device, satisfying:
// - Hardware version 2.1 (version is printed on the board);
// - Firmware built from the Zedmon repository's revision cdc9458f45, or
// equivalent.
//
// The Zedmon must be connected to a test device that:
// - Consumes a nontrivial amount of power (>1W will certainly suffice);
// - Consumes nontrial power within 1 second of being connected to power;
//
// Zedmon's relay must be on (its default state) at the beginning of the test.
// The test device will be power-cycled in the course of testing.
Future<void> main() async {
String zedmonPath;
ZedmonDescription zedmonDescription;
Directory tempDir;
String tempFilePath;
setUpAll(() async {
zedmonPath = Platform.script.resolve('runtime_deps/zedmon').toFilePath();
zedmonDescription = await getZedmonDescription(zedmonPath);
tempDir = await Directory.current.createTemp();
tempFilePath = '${tempDir.path}/zedmon.csv';
});
tearDown(() async {
final file = File(tempFilePath);
if (file.existsSync()) {
file.deleteSync();
}
});
// `zedmon list` should yield exactly one word, containing a serial number.
test('zedmon list', () async {
final result = await Process.run(zedmonPath, ['list']);
expect(result.exitCode, equals(0));
final regex = RegExp(r'\W+');
expect(regex.allMatches(result.stdout).length, equals(1));
});
// Records 1 second of Zedmon data and validates the power calculation for
// each line of output.
test('zedmon record', () async {
final result = await Process.run(
zedmonPath, ['record', '--out', tempFilePath, '--duration', '1s']);
expect(result.exitCode, equals(0));
final lines = await File(tempFilePath).readAsLines();
// Zedmon's nominal output rate is about 1500 Hz. Expecting 1400 lines
// gives a bit of buffer for packet loss; see fxbug.dev/64161.
expect(lines.length, greaterThan(1400));
for (String line in lines) {
validateZedmonCsvLine(line, zedmonDescription, 1e-4);
}
});
test('zedmon record downsampled', () async {
final result = await Process.run(zedmonPath, [
'record',
'--out',
tempFilePath,
'--duration',
'1s',
'--interval',
'100ms'
]);
expect(result.exitCode, equals(0));
final lines = await File(tempFilePath).readAsLines();
// We should see exactly 10 records, given a 100ms reporting interval over
// a 1s duration. (Zedmon's nominal reporting interval is ~667us, so we'd
// have to miss many consecutive packets before a downsampled packet is
// skipped, and that would indicate a problem worth investigating.)
expect(lines.length, equals(10));
for (String line in lines) {
// Power in each output record is averaged from the power derived from
// each sample rather than computed from average shunt voltage and
// average bus power. Consequently, the tolerance in the power calculation
// needs to be higher here.
validateZedmonCsvLine(line, zedmonDescription, 0.01);
}
});
// Tests that the 5-second average power drops by at least 99% when the
// relay is turned off.
test('zedmon relay', () async {
var result = await Process.run(zedmonPath, ['relay', 'off']);
expect(result.exitCode, equals(0));
sleep(Duration(seconds: 1));
final offPower = await measureAveragePower(
zedmonPath, tempFilePath, zedmonDescription, 5);
result = await Process.run(zedmonPath, ['relay', 'on']);
expect(result.exitCode, equals(0));
sleep(Duration(seconds: 1));
final onPower = await measureAveragePower(
zedmonPath, tempFilePath, zedmonDescription, 5);
expect(offPower, lessThan(0.01 * onPower));
});
}