blob: 330c2991e79a8913580e692a5017a70092f6db5d [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, LineSplitter, utf8;
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) {
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, 1e-4));
}
// Returns the average power measured by `zedmon record` over 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, '--duration', '${seconds}s']);
expect(result.exitCode, 0);
bool initialized = false;
int firstTimestampMicros = 0;
int prevTimestampMicros = 0;
double prevPower = 0.0;
double totalEnergy = 0.0;
await for (String line in File(tempFilePath)
.openRead()
.transform(utf8.decoder)
.transform(LineSplitter())) {
final parts = line.split(',');
final timestampMicros = int.parse(parts[desc.timestampIndex]);
final power = double.parse(parts[desc.powerIndex]);
if (initialized) {
final dt = timestampMicros - prevTimestampMicros;
// Use the trapezoid rule to estimate energy consumed since the previous
// sample.
totalEnergy += dt * (power + prevPower) / 2.0;
prevTimestampMicros = timestampMicros;
prevPower = power;
} else {
initialized = true;
firstTimestampMicros = timestampMicros;
}
prevTimestampMicros = timestampMicros;
prevPower = power;
}
return totalEnergy / (prevTimestampMicros - firstTimestampMicros);
}
// 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, 0);
final regex = RegExp(r'\W+');
expect(regex.allMatches(result.stdout).length, 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, 0);
var csvLinesRead = 0;
await for (String line in File(tempFilePath)
.openRead()
.transform(utf8.decoder)
.transform(LineSplitter())) {
validateZedmonCsvLine(line, zedmonDescription);
csvLinesRead += 1;
}
expect(csvLinesRead, greaterThan(1000));
});
// 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, 0);
sleep(Duration(seconds: 1));
final offPower = await measureAveragePower(
zedmonPath, tempFilePath, zedmonDescription, 5);
result = await Process.run(zedmonPath, ['relay', 'on']);
expect(result.exitCode, 0);
sleep(Duration(seconds: 1));
final onPower = await measureAveragePower(
zedmonPath, tempFilePath, zedmonDescription, 5);
expect(offPower, lessThan(0.01 * onPower));
});
}