Skip to content

Commit

Permalink
feat(ci): run some end to end tests on CI (#18)
Browse files Browse the repository at this point in the history
* refactor: move tests to more appropriate package

* refactor: split process vs. input stream handling of powermetrics input

* chore(tests): add CI-specific tests that can run against resources

… instead of running the real process, which isn't possible in CI
because this requires sudo access.

* feat(ci): only activate CI-specific tests

* refactor: clean-up
  • Loading branch information
metacosm committed Jan 26, 2024
1 parent 21503b4 commit 6b1eee2
Show file tree
Hide file tree
Showing 13 changed files with 193 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package --file pom.xml
run: mvn -B package -Dquarkus.test.profile.tags='ci' --file pom.xml

# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
# - name: Update dependency graph
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ public class PowerMeasurer {

public Multi<SensorMeasure> startTracking(String pid) throws Exception {
// first make sure that the process with that pid exists
final var parsedPID = Long.parseLong(pid);
ProcessHandle.of(parsedPID).orElseThrow(() -> new IllegalArgumentException("Unknown process: " + pid));
final var parsedPID = validPIDOrFail(pid);

if (!sensor.isStarted()) {
sensor.start(SAMPLING_FREQUENCY_IN_MILLIS);
Expand All @@ -42,6 +41,12 @@ public Multi<SensorMeasure> startTracking(String pid) throws Exception {
.onCancellation().invoke(() -> sensor.unregister(registeredPID));
}

protected long validPIDOrFail(String pid) {
final var parsedPID = Long.parseLong(pid);
ProcessHandle.of(parsedPID).orElseThrow(() -> new IllegalArgumentException("Unknown process: " + pid));
return parsedPID;
}

public SensorMetadata metadata() {
return sensor.metadata();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import jakarta.inject.Singleton;

import io.github.metacosm.power.sensors.linux.rapl.IntelRAPLSensor;
import io.github.metacosm.power.sensors.macos.powermetrics.MacOSPowermetricsSensor;
import io.github.metacosm.power.sensors.macos.powermetrics.ProcessMacOSPowermetricsSensor;

@Singleton
public class PowerSensorProducer {
Expand All @@ -17,7 +17,7 @@ public PowerSensor sensor() {

public static PowerSensor determinePowerSensor() {
if (OS_NAME.contains("mac os x")) {
return new MacOSPowermetricsSensor();
return new ProcessMacOSPowermetricsSensor();
}

if (!OS_NAME.contains("linux")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,23 @@
import io.github.metacosm.power.sensors.PowerSensor;
import io.github.metacosm.power.sensors.RegisteredPID;

public class MacOSPowermetricsSensor implements PowerSensor {
public abstract class MacOSPowermetricsSensor implements PowerSensor {
public static final String CPU = "CPU";
public static final String GPU = "GPU";
public static final String ANE = "ANE";
@SuppressWarnings("unused")
public static final String DRAM = "DRAM";
@SuppressWarnings("unused")
public static final String DCS = "DCS";
public static final String PACKAGE = "Package";
public static final String CPU_SHARE = "cpuShare";

private Process powermetrics;
private final Measures measures = new MapMeasures();
private final CPU cpu;
private CPU cpu;

public MacOSPowermetricsSensor() {
// extract metadata
try {
final var exec = new ProcessBuilder()
.command("sudo", "powermetrics", "--samplers", "cpu_power", "-i", "10", "-n", "1")
.start();
this.cpu = initMetadata(exec.getInputStream());
} catch (Exception e) {
throw new RuntimeException("Couldn't execute powermetrics to extract metadata", e);
}
}

MacOSPowermetricsSensor(InputStream inputStream) {
this.cpu = initMetadata(inputStream);
}

CPU initMetadata(InputStream inputStream) {
void initMetadata(InputStream inputStream) {
try (BufferedReader input = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
CPU cpu = null;
Map<String, SensorMetadata.ComponentMetadata> components = new HashMap<>();
while ((line = input.readLine()) != null) {
if (cpu == null) {
Expand Down Expand Up @@ -78,7 +62,6 @@ CPU initMetadata(InputStream inputStream) {
final var metadata = new SensorMetadata(components,
"macOS powermetrics derived information, see https://firefox-source-docs.mozilla.org/performance/powermetrics.html");
cpu.setMetadata(metadata);
return cpu;
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand Down Expand Up @@ -200,29 +183,12 @@ Measures extractPowerMeasure(InputStream powerMeasureInput, Long tick) {
return measures;
}

public void start(long frequency) throws Exception {
if (!isStarted()) {
// it takes some time for the external process in addition to the sampling time so adjust the sampling frequency to account for this so that at most one measure occurs during the sampling time window
final var freq = Long.toString(frequency - 50);
powermetrics = new ProcessBuilder().command("sudo", "powermetrics", "--samplers", "cpu_power,tasks",
"--show-process-samp-norm", "--show-process-gpu", "-i", freq).start();
}
}

@Override
public boolean isStarted() {
return powermetrics != null && powermetrics.isAlive();
}

@Override
public Measures update(Long tick) {
return extractPowerMeasure(powermetrics.getInputStream(), tick);
return extractPowerMeasure(getInputStream(), tick);
}

@Override
public void stop() {
powermetrics.destroy();
}
protected abstract InputStream getInputStream();

@Override
public void unregister(RegisteredPID registeredPID) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.github.metacosm.power.sensors.macos.powermetrics;

import java.io.InputStream;

public class ProcessMacOSPowermetricsSensor extends MacOSPowermetricsSensor {
private Process powermetrics;

public ProcessMacOSPowermetricsSensor() {
// extract metadata
try {
final var exec = new ProcessBuilder()
.command("sudo", "powermetrics", "--samplers", "cpu_power", "-i", "10", "-n", "1")
.start();
initMetadata(exec.getInputStream());
} catch (Exception e) {
throw new RuntimeException("Couldn't execute powermetrics to extract metadata", e);
}
}

public void start(long frequency) throws Exception {
if (!isStarted()) {
// it takes some time for the external process in addition to the sampling time so adjust the sampling frequency to account for this so that at most one measure occurs during the sampling time window
final var freq = Long.toString(frequency - 50);
powermetrics = new ProcessBuilder().command("sudo", "powermetrics", "--samplers", "cpu_power,tasks",
"--show-process-samp-norm", "--show-process-gpu", "-i", freq).start();
}
}

@Override
public boolean isStarted() {
return powermetrics != null && powermetrics.isAlive();
}

@Override
protected InputStream getInputStream() {
return powermetrics.getInputStream();
}

@Override
public void stop() {
powermetrics.destroy();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.github.metacosm.power.sensors.macos.powermetrics;

import java.io.InputStream;

public class ResourceMacOSPowermetricsSensor extends MacOSPowermetricsSensor {
private final String resourceName;
private boolean started;

public ResourceMacOSPowermetricsSensor(String resourceName) {
this.resourceName = resourceName;
initMetadata(getInputStream());
}

@Override
protected InputStream getInputStream() {
return Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName);
}

@Override
public boolean isStarted() {
return started;
}

@Override
public void start(long samplingFrequencyInMillis) {
if (!started) {
started = true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.github.metacosm.power;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.*;

import java.util.Set;

import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;

@QuarkusTest
@TestProfile(CIQuarkusTestProfile.class)
public class CIPowerResourceTest {

protected long getPid() {
return 29419;
}

@Test
public void testPowerEndpoint() {
final var pid = getPid();
given()
.when().get("/power/" + pid)
.then()
.statusCode(200);
}

@Test
public void testMacOSAppleSiliconMetadataEndpoint() {
final var metadata = given()
.when().get("/power/metadata")
.then()
.statusCode(200)
.extract().body().as(SensorMetadata.class);
assertEquals(4, metadata.componentCardinality());
assertTrue(metadata.documentation().contains("powermetrics"));
assertTrue(metadata.components().keySet().containsAll(Set.of("CPU", "GPU", "ANE", "cpuShare")));

final var cpu = metadata.metadataFor("CPU");
assertEquals(0, cpu.index());
assertEquals("CPU", cpu.name());
assertEquals("mW", cpu.unit());
assertTrue(cpu.isAttributed());

final var cpuShare = metadata.metadataFor("cpuShare");
assertEquals(3, cpuShare.index());
assertEquals("cpuShare", cpuShare.name());
assertEquals("decimal percentage", cpuShare.unit());
assertFalse(cpuShare.isAttributed());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.metacosm.power;

import java.util.Set;

import io.quarkus.test.junit.QuarkusTestProfile;

public class CIQuarkusTestProfile implements QuarkusTestProfile {

@Override
public Set<String> tags() {
return Set.of("ci");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.metacosm.power;

import io.quarkus.test.Mock;

@Mock
@SuppressWarnings("unused")
public class MockPowerMeasurer extends PowerMeasurer {

@Override
protected long validPIDOrFail(String pid) {
return Long.parseLong(pid);
}
}
12 changes: 12 additions & 0 deletions server/src/test/java/io/github/metacosm/power/MockPowerSensor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.github.metacosm.power;

import io.github.metacosm.power.sensors.macos.powermetrics.ResourceMacOSPowermetricsSensor;
import io.quarkus.test.Mock;

@Mock
@SuppressWarnings("unused")
public class MockPowerSensor extends ResourceMacOSPowermetricsSensor {
public MockPowerSensor() {
super("sonoma-m1max.txt");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.metacosm;
package io.github.metacosm.power;

import io.quarkus.test.junit.QuarkusIntegrationTest;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.metacosm;
package io.github.metacosm.power;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.*;
Expand All @@ -10,21 +10,24 @@
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

import io.github.metacosm.power.SensorMetadata;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class PowerResourceTest {

@Test
public void testPowerEndpoint() {
final var pid = ProcessHandle.current().pid();
final var pid = getPid();
given()
.when().get("/power/" + pid)
.then()
.statusCode(200);
}

protected long getPid() {
return ProcessHandle.current().pid();
}

@Test
@EnabledOnOs(OS.MAC)
@EnabledIfSystemProperty(named = "os.arch", matches = "aarch64")
Expand Down
Loading

0 comments on commit 6b1eee2

Please sign in to comment.