From 54fc72b3457aedaf074091837f611b914f7262b9 Mon Sep 17 00:00:00 2001 From: Graham Gilbert Date: Tue, 27 Jan 2026 09:14:10 -0800 Subject: [PATCH 1/6] Energy impact table --- .claude/settings.local.json | 10 + BUILD.bazel | 1 + main.go | 2 + tables/energyimpact/BUILD.bazel | 25 +++ tables/energyimpact/energy_impact.go | 155 +++++++++++++++ tables/energyimpact/energy_impact_test.go | 125 ++++++++++++ .../test_powermetrics_output.plist | 186 ++++++++++++++++++ 7 files changed, 504 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 tables/energyimpact/BUILD.bazel create mode 100644 tables/energyimpact/energy_impact.go create mode 100644 tables/energyimpact/energy_impact_test.go create mode 100644 tables/energyimpact/test_powermetrics_output.plist diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..e3d6b64 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(sudo powermetrics:*)", + "Bash(bazel test:*)", + "Bash(bazel build:*)" + ] + } +} diff --git a/BUILD.bazel b/BUILD.bazel index 559748d..bb87762 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -41,6 +41,7 @@ go_library( "//tables/authdb", "//tables/chromeuserprofiles", "//tables/crowdstrike_falcon", + "//tables/energyimpact", "//tables/fileline", "//tables/filevaultusers", "//tables/localnetworkpermissions", diff --git a/main.go b/main.go index 531f522..b223982 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "github.com/macadmins/osquery-extension/tables/alt_system_info" "github.com/macadmins/osquery-extension/tables/chromeuserprofiles" "github.com/macadmins/osquery-extension/tables/crowdstrike_falcon" + "github.com/macadmins/osquery-extension/tables/energyimpact" "github.com/macadmins/osquery-extension/tables/fileline" "github.com/macadmins/osquery-extension/tables/filevaultusers" "github.com/macadmins/osquery-extension/tables/localnetworkpermissions" @@ -92,6 +93,7 @@ func main() { if runtime.GOOS == "darwin" { darwinPlugins := []osquery.OsqueryPlugin{ + table.NewPlugin("energy_impact", energyimpact.EnergyImpactColumns(), energyimpact.EnergyImpactGenerate), table.NewPlugin("filevault_users", filevaultusers.FileVaultUsersColumns(), filevaultusers.FileVaultUsersGenerate), table.NewPlugin("local_network_permissions", localnetworkpermissions.LocalNetworkPermissionsColumns(), localnetworkpermissions.LocalNetworkPermissionsGenerate), table.NewPlugin("macos_profiles", macosprofiles.MacOSProfilesColumns(), macosprofiles.MacOSProfilesGenerate), diff --git a/tables/energyimpact/BUILD.bazel b/tables/energyimpact/BUILD.bazel new file mode 100644 index 0000000..2d2a597 --- /dev/null +++ b/tables/energyimpact/BUILD.bazel @@ -0,0 +1,25 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "energyimpact", + srcs = ["energy_impact.go"], + importpath = "github.com/macadmins/osquery-extension/tables/energyimpact", + visibility = ["//visibility:public"], + deps = [ + "//pkg/utils", + "@com_github_micromdm_plist//:plist", + "@com_github_osquery_osquery_go//plugin/table", + "@com_github_pkg_errors//:errors", + ], +) + +go_test( + name = "energyimpact_test", + srcs = ["energy_impact_test.go"], + embed = [":energyimpact"], + embedsrcs = ["test_powermetrics_output.plist"], + deps = [ + "//pkg/utils", + "@com_github_stretchr_testify//assert", + ], +) diff --git a/tables/energyimpact/energy_impact.go b/tables/energyimpact/energy_impact.go new file mode 100644 index 0000000..0d5f67a --- /dev/null +++ b/tables/energyimpact/energy_impact.go @@ -0,0 +1,155 @@ +package energyimpact + +import ( + "context" + "fmt" + "os" + "strconv" + + "github.com/macadmins/osquery-extension/pkg/utils" + "github.com/micromdm/plist" + "github.com/osquery/osquery-go/plugin/table" + "github.com/pkg/errors" +) + +const defaultInterval = 1000 + +// powermetricsOutput represents the top-level plist structure from powermetrics +type powermetricsOutput struct { + Tasks []task `plist:"tasks"` +} + +// task represents individual process data from powermetrics +type task struct { + PID int `plist:"pid"` + Name string `plist:"name"` + EnergyImpact float64 `plist:"energy_impact"` + EnergyImpactPerS float64 `plist:"energy_impact_per_s"` + CPUTimeNS int64 `plist:"cputime_ns"` + CPUTimeMSPerS float64 `plist:"cputime_ms_per_s"` + CPUTimeUserlandRatio float64 `plist:"cputime_userland_ratio"` + IntrWakeups int `plist:"intr_wakeups"` + IntrWakeupsPerS float64 `plist:"intr_wakeups_per_s"` + IdleWakeups int `plist:"idle_wakeups"` + IdleWakeupsPerS float64 `plist:"idle_wakeups_per_s"` + DiskIOBytesRead int64 `plist:"diskio_bytesread"` + DiskIOBytesReadPerS float64 `plist:"diskio_bytesread_per_s"` + DiskIOBytesWritten int64 `plist:"diskio_byteswritten"` + DiskIOBytesWrittenPerS float64 `plist:"diskio_byteswritten_per_s"` + PacketsReceived int `plist:"packets_received"` + PacketsSent int `plist:"packets_sent"` + BytesReceived int64 `plist:"bytes_received"` + BytesSent int64 `plist:"bytes_sent"` +} + +// EnergyImpactColumns returns the column definitions for the energy_impact table +func EnergyImpactColumns() []table.ColumnDefinition { + return []table.ColumnDefinition{ + table.IntegerColumn("pid"), + table.TextColumn("name"), + table.TextColumn("energy_impact"), + table.TextColumn("energy_impact_per_s"), + table.IntegerColumn("cputime_ns"), + table.TextColumn("cputime_ms_per_s"), + table.TextColumn("cputime_userland_ratio"), + table.IntegerColumn("intr_wakeups"), + table.TextColumn("intr_wakeups_per_s"), + table.IntegerColumn("idle_wakeups"), + table.TextColumn("idle_wakeups_per_s"), + table.IntegerColumn("diskio_bytesread"), + table.TextColumn("diskio_bytesread_per_s"), + table.IntegerColumn("diskio_byteswritten"), + table.TextColumn("diskio_byteswritten_per_s"), + table.IntegerColumn("packets_received"), + table.IntegerColumn("packets_sent"), + table.IntegerColumn("bytes_received"), + table.IntegerColumn("bytes_sent"), + table.IntegerColumn("interval"), + } +} + +// EnergyImpactGenerate generates the table data when queried +func EnergyImpactGenerate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { + var results []map[string]string + + // Get interval from WHERE clause, default to 1000ms + interval := defaultInterval + if constraintList, present := queryContext.Constraints["interval"]; present { + for _, constraint := range constraintList.Constraints { + if constraint.Operator == table.OperatorEquals { + if parsed, err := strconv.Atoi(constraint.Expression); err == nil { + interval = parsed + } + } + } + } + + r := utils.NewRunner() + fs := utils.OSFileSystem{} + tasks, err := runPowermetrics(r, fs, interval) + if err != nil { + fmt.Println(err) + return results, err + } + + for _, t := range tasks { + results = append(results, map[string]string{ + "pid": strconv.Itoa(t.PID), + "name": t.Name, + "energy_impact": fmt.Sprintf("%.2f", t.EnergyImpact), + "energy_impact_per_s": fmt.Sprintf("%.2f", t.EnergyImpactPerS), + "cputime_ns": strconv.FormatInt(t.CPUTimeNS, 10), + "cputime_ms_per_s": fmt.Sprintf("%.2f", t.CPUTimeMSPerS), + "cputime_userland_ratio": fmt.Sprintf("%.2f", t.CPUTimeUserlandRatio), + "intr_wakeups": strconv.Itoa(t.IntrWakeups), + "intr_wakeups_per_s": fmt.Sprintf("%.2f", t.IntrWakeupsPerS), + "idle_wakeups": strconv.Itoa(t.IdleWakeups), + "idle_wakeups_per_s": fmt.Sprintf("%.2f", t.IdleWakeupsPerS), + "diskio_bytesread": strconv.FormatInt(t.DiskIOBytesRead, 10), + "diskio_bytesread_per_s": fmt.Sprintf("%.2f", t.DiskIOBytesReadPerS), + "diskio_byteswritten": strconv.FormatInt(t.DiskIOBytesWritten, 10), + "diskio_byteswritten_per_s": fmt.Sprintf("%.2f", t.DiskIOBytesWrittenPerS), + "packets_received": strconv.Itoa(t.PacketsReceived), + "packets_sent": strconv.Itoa(t.PacketsSent), + "bytes_received": strconv.FormatInt(t.BytesReceived, 10), + "bytes_sent": strconv.FormatInt(t.BytesSent, 10), + "interval": strconv.Itoa(interval), + }) + } + + return results, nil +} + +// runPowermetrics executes the powermetrics command and parses the output +func runPowermetrics(r utils.Runner, fs utils.FileSystem, interval int) ([]task, error) { + var output powermetricsOutput + + // Check if powermetrics binary exists + _, err := fs.Stat("/usr/bin/powermetrics") + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + return nil, err + } + + // Run powermetrics command + out, err := r.Runner.RunCmd( + "/usr/bin/powermetrics", + "-f", "plist", + "-n", "1", + "-i", strconv.Itoa(interval), + "--samplers", "tasks", + "--show-process-energy", + ) + if err != nil { + return nil, errors.Wrap(err, "running powermetrics") + } + + // Parse plist output + if err := plist.Unmarshal(out, &output); err != nil { + return nil, errors.Wrap(err, "unmarshalling powermetrics output") + } + + return output.Tasks, nil +} diff --git a/tables/energyimpact/energy_impact_test.go b/tables/energyimpact/energy_impact_test.go new file mode 100644 index 0000000..5388c85 --- /dev/null +++ b/tables/energyimpact/energy_impact_test.go @@ -0,0 +1,125 @@ +package energyimpact + +import ( + _ "embed" + "errors" + "testing" + + "github.com/macadmins/osquery-extension/pkg/utils" + "github.com/stretchr/testify/assert" +) + +//go:embed test_powermetrics_output.plist +var testPlist string + +func TestRunPowermetrics(t *testing.T) { + tests := []struct { + name string + mockCmd utils.MockCmdRunner + fileExist bool + interval int + wantErr bool + wantTasks int + checkFirst func(t *testing.T, tasks []task) + }{ + { + name: "Binary not present", + mockCmd: utils.MockCmdRunner{ + Output: "", + Err: nil, + }, + fileExist: false, + interval: 1000, + wantErr: false, + wantTasks: 0, + }, + { + name: "Successful execution", + mockCmd: utils.MockCmdRunner{ + Output: testPlist, + Err: nil, + }, + fileExist: true, + interval: 1000, + wantErr: false, + wantTasks: 3, + checkFirst: func(t *testing.T, tasks []task) { + // Verify first task (Safari) + assert.Equal(t, 1234, tasks[0].PID) + assert.Equal(t, "Safari", tasks[0].Name) + assert.InDelta(t, 125.5, tasks[0].EnergyImpact, 0.01) + assert.InDelta(t, 12.55, tasks[0].EnergyImpactPerS, 0.01) + assert.Equal(t, int64(500000000), tasks[0].CPUTimeNS) + assert.InDelta(t, 50.5, tasks[0].CPUTimeMSPerS, 0.01) + assert.InDelta(t, 0.75, tasks[0].CPUTimeUserlandRatio, 0.01) + assert.Equal(t, 100, tasks[0].IntrWakeups) + assert.InDelta(t, 10.5, tasks[0].IntrWakeupsPerS, 0.01) + assert.Equal(t, 50, tasks[0].IdleWakeups) + assert.InDelta(t, 5.2, tasks[0].IdleWakeupsPerS, 0.01) + assert.Equal(t, int64(1048576), tasks[0].DiskIOBytesRead) + assert.InDelta(t, 104857.6, tasks[0].DiskIOBytesReadPerS, 0.01) + assert.Equal(t, int64(524288), tasks[0].DiskIOBytesWritten) + assert.InDelta(t, 52428.8, tasks[0].DiskIOBytesWrittenPerS, 0.01) + assert.Equal(t, 200, tasks[0].PacketsReceived) + assert.Equal(t, 150, tasks[0].PacketsSent) + assert.Equal(t, int64(204800), tasks[0].BytesReceived) + assert.Equal(t, int64(102400), tasks[0].BytesSent) + + // Verify DEAD_TASKS entry + assert.Equal(t, -1, tasks[2].PID) + assert.Equal(t, "DEAD_TASKS", tasks[2].Name) + }, + }, + { + name: "Command execution error", + mockCmd: utils.MockCmdRunner{ + Output: "", + Err: errors.New("command error"), + }, + fileExist: true, + interval: 1000, + wantErr: true, + wantTasks: 0, + }, + { + name: "Invalid plist output", + mockCmd: utils.MockCmdRunner{ + Output: "invalid plist data", + Err: nil, + }, + fileExist: true, + interval: 1000, + wantErr: true, + wantTasks: 0, + }, + { + name: "Custom interval", + mockCmd: utils.MockCmdRunner{ + Output: testPlist, + Err: nil, + }, + fileExist: true, + interval: 5000, + wantErr: false, + wantTasks: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runner := utils.Runner{Runner: tt.mockCmd} + fs := utils.MockFileSystem{FileExists: tt.fileExist} + + tasks, err := runPowermetrics(runner, fs, tt.interval) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Len(t, tasks, tt.wantTasks) + if tt.checkFirst != nil && len(tasks) > 0 { + tt.checkFirst(t, tasks) + } + } + }) + } +} diff --git a/tables/energyimpact/test_powermetrics_output.plist b/tables/energyimpact/test_powermetrics_output.plist new file mode 100644 index 0000000..e81c8a3 --- /dev/null +++ b/tables/energyimpact/test_powermetrics_output.plist @@ -0,0 +1,186 @@ + + + + + tasks + + + pid + 1234 + name + Safari + started_abstime_ns + 123456789 + interval_ns + 1000000000 + cputime_ns + 500000000 + cputime_ms_per_s + 50.5 + cputime_userland_ratio + 0.75 + cputime_sample_ms_per_s + 45.2 + intr_wakeups + 100 + intr_wakeups_per_s + 10.5 + idle_wakeups + 50 + idle_wakeups_per_s + 5.2 + timer_wakeups + + interval_ns + 1000000 + wakeups + 25 + wakeups_per_s + 2.5 + + diskio_bytesread + 1048576 + diskio_bytesread_per_s + 104857.6 + diskio_byteswritten + 524288 + diskio_byteswritten_per_s + 52428.8 + pageins + 10 + pageins_per_s + 1.0 + packets_received + 200 + packets_sent + 150 + bytes_received + 204800 + bytes_sent + 102400 + energy_impact + 125.5 + energy_impact_per_s + 12.55 + + + pid + 5678 + name + Chrome + started_abstime_ns + 987654321 + interval_ns + 1000000000 + cputime_ns + 750000000 + cputime_ms_per_s + 75.0 + cputime_userland_ratio + 0.85 + cputime_sample_ms_per_s + 70.1 + intr_wakeups + 200 + intr_wakeups_per_s + 20.0 + idle_wakeups + 80 + idle_wakeups_per_s + 8.0 + timer_wakeups + + interval_ns + 1000000 + wakeups + 40 + wakeups_per_s + 4.0 + + diskio_bytesread + 2097152 + diskio_bytesread_per_s + 209715.2 + diskio_byteswritten + 1048576 + diskio_byteswritten_per_s + 104857.6 + pageins + 20 + pageins_per_s + 2.0 + packets_received + 500 + packets_sent + 300 + bytes_received + 512000 + bytes_sent + 256000 + energy_impact + 250.75 + energy_impact_per_s + 25.075 + + + pid + -1 + name + DEAD_TASKS + started_abstime_ns + 0 + interval_ns + 1000000000 + cputime_ns + 100000000 + cputime_ms_per_s + 10.0 + cputime_userland_ratio + 0.5 + cputime_sample_ms_per_s + 8.5 + intr_wakeups + 30 + intr_wakeups_per_s + 3.0 + idle_wakeups + 15 + idle_wakeups_per_s + 1.5 + timer_wakeups + + interval_ns + 1000000 + wakeups + 5 + wakeups_per_s + 0.5 + + diskio_bytesread + 0 + diskio_bytesread_per_s + 0.0 + diskio_byteswritten + 0 + diskio_byteswritten_per_s + 0.0 + pageins + 0 + pageins_per_s + 0.0 + packets_received + 0 + packets_sent + 0 + bytes_received + 0 + bytes_sent + 0 + energy_impact + 5.25 + energy_impact_per_s + 0.525 + + + + From 87b5dd17518ce4903310163b4ddb740d166c70f4 Mon Sep 17 00:00:00 2001 From: Graham Gilbert Date: Tue, 27 Jan 2026 09:14:39 -0800 Subject: [PATCH 2/6] ignore --- .claude/settings.local.json | 10 ---------- .gitignore | 3 ++- 2 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index e3d6b64..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(find:*)", - "Bash(sudo powermetrics:*)", - "Bash(bazel test:*)", - "Bash(bazel build:*)" - ] - } -} diff --git a/.gitignore b/.gitignore index e2be26b..d6fa0e3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ config.mk bazel-* coverage/* version_config.go -cover.out \ No newline at end of file +cover.out +.claude/settings.local.json \ No newline at end of file From bf46c2541c9b9fe9c056f2262a376e87dbe6ba8e Mon Sep 17 00:00:00 2001 From: Graham Gilbert Date: Tue, 27 Jan 2026 09:17:25 -0800 Subject: [PATCH 3/6] readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fe0cfd4..d6d5a58 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ For production deployment, you should refer to the [osquery documentation](https |------------------------------| --------------------------------------------------------------------------------------------- |-------------------------| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `alt_system_info` | Alternative system_info table | macOS | This table is an alternative to the built-in system_info table in osquery, which triggers an `Allow "osquery" to find devices on local networks?` prompt on macOS 15.0. On versions other than 15.0, this table falls back to the built-in system_info table. Note: this table returns an empty `cpu_subtype` field. See [#58](https://github.com/macadmins/osquery-extension/pull/58) for more details. | | `authdb` | macOS Authorization database | macOS | Use the constraint `name` to specify a right name to query, otherwise all rights will be returned. | -| `crowdstrike_falcon` | Provides basic information about the currently installed Falcon sensor. | Linux / macOS | Requires Falcon to be installed. | +| `crowdstrike_falcon` | Provides basic information about the currently installed Falcon sensor. | Linux / macOS | Requires Falcon to be installed. | +| `energy_impact` | Process energy impact data from `powermetrics` | macOS | Use the `interval` constraint to specify sampling duration in milliseconds (default: 1000ms). | | `file_lines` | Read an arbitrary file | Linux / macOS / Windows | Use the constraint `path` and `last` to specify the file to read lines from | | `filevault_users` | Information on the users able to unlock the current boot volume when encrypted with Filevault | macOS | | | `google_chrome_profiles` | Profiles configured in Google Chrome. | Linux / macOS / Windows | | From 0adbedc44372bd579b89c9d8cb1d2a12601e05b0 Mon Sep 17 00:00:00 2001 From: Graham Gilbert Date: Tue, 27 Jan 2026 09:34:30 -0800 Subject: [PATCH 4/6] More unit tests --- tables/energyimpact/BUILD.bazel | 1 + tables/energyimpact/energy_impact_test.go | 61 ++++++++++++++++++++--- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/tables/energyimpact/BUILD.bazel b/tables/energyimpact/BUILD.bazel index 2d2a597..a7a81f6 100644 --- a/tables/energyimpact/BUILD.bazel +++ b/tables/energyimpact/BUILD.bazel @@ -20,6 +20,7 @@ go_test( embedsrcs = ["test_powermetrics_output.plist"], deps = [ "//pkg/utils", + "@com_github_osquery_osquery_go//plugin/table", "@com_github_stretchr_testify//assert", ], ) diff --git a/tables/energyimpact/energy_impact_test.go b/tables/energyimpact/energy_impact_test.go index 5388c85..9dd6ec2 100644 --- a/tables/energyimpact/energy_impact_test.go +++ b/tables/energyimpact/energy_impact_test.go @@ -1,26 +1,73 @@ package energyimpact import ( + "context" _ "embed" "errors" "testing" "github.com/macadmins/osquery-extension/pkg/utils" + "github.com/osquery/osquery-go/plugin/table" "github.com/stretchr/testify/assert" ) //go:embed test_powermetrics_output.plist var testPlist string +func TestEnergyImpactColumns(t *testing.T) { + columns := EnergyImpactColumns() + + // Should return 20 columns + assert.Len(t, columns, 20) + + // Verify column names exist + columnNames := make(map[string]bool) + for _, col := range columns { + columnNames[col.Name] = true + } + + expectedColumns := []string{ + "pid", "name", "energy_impact", "energy_impact_per_s", + "cputime_ns", "cputime_ms_per_s", "cputime_userland_ratio", + "intr_wakeups", "intr_wakeups_per_s", "idle_wakeups", "idle_wakeups_per_s", + "diskio_bytesread", "diskio_bytesread_per_s", + "diskio_byteswritten", "diskio_byteswritten_per_s", + "packets_received", "packets_sent", "bytes_received", "bytes_sent", + "interval", + } + + for _, name := range expectedColumns { + assert.True(t, columnNames[name], "Expected column %s not found", name) + } +} + +func TestEnergyImpactGenerate(t *testing.T) { + // This test verifies that the generate function works with the default context + // Since it requires actual powermetrics execution, we only test the function signature + ctx := context.Background() + queryContext := table.QueryContext{ + Constraints: make(map[string]table.ConstraintList), + } + + // Call the function - it may return empty results if powermetrics isn't available + // or require root, but it shouldn't panic + results, err := EnergyImpactGenerate(ctx, queryContext) + + // The function should return without panicking + // Results may be empty if not running as root or powermetrics doesn't exist + assert.NotNil(t, results) + _ = err // Error is acceptable if powermetrics can't run +} + func TestRunPowermetrics(t *testing.T) { tests := []struct { - name string - mockCmd utils.MockCmdRunner - fileExist bool - interval int - wantErr bool - wantTasks int - checkFirst func(t *testing.T, tasks []task) + name string + mockCmd utils.MockCmdRunner + fileExist bool + interval int + wantErr bool + wantTasks int + checkFirst func(t *testing.T, tasks []task) }{ { name: "Binary not present", From 7ccbf4d9c4e053e61eece3d6d698b965ab984912 Mon Sep 17 00:00:00 2001 From: Graham Gilbert Date: Tue, 27 Jan 2026 09:37:14 -0800 Subject: [PATCH 5/6] test --- tables/energyimpact/energy_impact_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tables/energyimpact/energy_impact_test.go b/tables/energyimpact/energy_impact_test.go index 9dd6ec2..22cf6f4 100644 --- a/tables/energyimpact/energy_impact_test.go +++ b/tables/energyimpact/energy_impact_test.go @@ -54,9 +54,14 @@ func TestEnergyImpactGenerate(t *testing.T) { results, err := EnergyImpactGenerate(ctx, queryContext) // The function should return without panicking - // Results may be empty if not running as root or powermetrics doesn't exist - assert.NotNil(t, results) - _ = err // Error is acceptable if powermetrics can't run + // On Linux: powermetrics doesn't exist, returns nil results with no error + // On macOS without root: returns error (requires superuser) + // On macOS with root: returns results + if err != nil { + // Error case (e.g., not running as root on macOS) + assert.Nil(t, results) + } + // If no error, results could be nil (binary not found) or populated (successful run) } func TestRunPowermetrics(t *testing.T) { From 0b89d6c14fc69eaaffc99c61485a39a7558e3c93 Mon Sep 17 00:00:00 2001 From: Graham Gilbert Date: Tue, 27 Jan 2026 09:37:28 -0800 Subject: [PATCH 6/6] version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 3a3cd8c..1892b92 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.1 +1.3.2