Skip to content

Commit d476221

Browse files
committed
clean output
Signed-off-by: Harper, Jason M <[email protected]>
1 parent 5c44fea commit d476221

File tree

2 files changed

+277
-6
lines changed

2 files changed

+277
-6
lines changed

cmd/config/restore.go

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ package config
66

77
import (
88
"bufio"
9+
"bytes"
910
"fmt"
11+
"io"
1012
"log/slog"
1113
"os"
1214
"os/exec"
@@ -114,7 +116,8 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error {
114116
// parse the configuration file
115117
flagValues, err := parseConfigFile(configFilePath)
116118
if err != nil {
117-
fmt.Fprintf(os.Stderr, "Error: failed to parse configuration file: %v\n", err)
119+
err = fmt.Errorf("failed to parse configuration file: %v", err)
120+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
118121
slog.Error(err.Error())
119122
cmd.SilenceUsage = true
120123
return err
@@ -135,7 +138,11 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error {
135138
// build the command to execute
136139
executable, err := os.Executable()
137140
if err != nil {
138-
return fmt.Errorf("failed to get executable path: %v", err)
141+
err = fmt.Errorf("failed to get executable path: %v", err)
142+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
143+
slog.Error(err.Error())
144+
cmd.SilenceUsage = true
145+
return err
139146
}
140147

141148
// build arguments: perfspect config --target ... --flag1 value1 --flag2 value2 ...
@@ -169,6 +176,9 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error {
169176
cmdArgs = append(cmdArgs, fmt.Sprintf("--%s", fv.flagName), fv.value)
170177
}
171178

179+
// always add --no-summary to avoid printing config summary before and after changes
180+
cmdArgs = append(cmdArgs, "--no-summary")
181+
172182
// show the command that will be executed
173183
fmt.Printf("Command: %s %s\n\n", executable, strings.Join(cmdArgs, " "))
174184

@@ -178,7 +188,11 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error {
178188
reader := bufio.NewReader(os.Stdin)
179189
response, err := reader.ReadString('\n')
180190
if err != nil {
181-
return fmt.Errorf("failed to read user input: %v", err)
191+
err = fmt.Errorf("failed to read user input: %v", err)
192+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
193+
slog.Error(err.Error())
194+
cmd.SilenceUsage = true
195+
return err
182196
}
183197
response = strings.TrimSpace(strings.ToLower(response))
184198
if response != "y" && response != "yes" {
@@ -193,15 +207,31 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error {
193207

194208
execCmd := exec.Command(executable, cmdArgs...)
195209
execCmd.Stdout = os.Stdout
196-
execCmd.Stderr = os.Stderr
197210
execCmd.Stdin = os.Stdin
198211

212+
// capture stderr for parsing
213+
var stderrBuf bytes.Buffer
214+
stderrWriter := io.MultiWriter(os.Stderr, &stderrBuf)
215+
execCmd.Stderr = stderrWriter
216+
199217
err = execCmd.Run()
218+
219+
// parse stderr output and present results in flag order
220+
parseAndPresentResults(stderrBuf.String(), flagValues)
221+
200222
if err != nil {
201223
if exitErr, ok := err.(*exec.ExitError); ok {
202-
return fmt.Errorf("config command failed with exit code %d", exitErr.ExitCode())
224+
err = fmt.Errorf("config command failed with exit code %d", exitErr.ExitCode())
225+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
226+
slog.Error(err.Error())
227+
cmd.SilenceUsage = true
228+
return err
203229
}
204-
return fmt.Errorf("failed to execute config command: %v", err)
230+
err = fmt.Errorf("failed to execute config command: %v", err)
231+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
232+
slog.Error(err.Error())
233+
cmd.SilenceUsage = true
234+
return err
205235
}
206236

207237
return nil
@@ -371,3 +401,64 @@ func parseEnableDisableOrOption(value string, validOptions []string) (string, er
371401

372402
return "", fmt.Errorf("invalid value '%s', valid options are: %s", value, strings.Join(validOptions, ", "))
373403
}
404+
405+
// parseAndPresentResults parses the stderr output from the config command and presents
406+
// successes and errors in the same order as the config flags were specified
407+
// example: "configuration update complete: set gov to powersave, set c1-demotion to disable, set tdp to 350, set c6 to enable, set epb to 0, set core-max to 3.2, set cores to 86, set elc to default, failed to set pref-l2hw to enable, set pref-dcuhw to enable, set pref-llc to disable, set pref-aop to enable, set pref-l2adj to enable, set uncore-max-compute to 2.2, failed to set llc to 336, set pref-dcunp to enable, set pref-homeless to enable, set pref-amp to enable, set pref-dcuip to enable, set pref-llcpp to enable, set uncore-max-io to 2.5, set uncore-min-compute to 0.8, set uncore-min-io to 0.8"
408+
func parseAndPresentResults(stderrOutput string, flagValues []flagValue) {
409+
if stderrOutput == "" {
410+
return
411+
}
412+
413+
// Parse stderr for success and error messages
414+
// Looking for patterns like:
415+
// - "set <flag> to <value>"
416+
// - "failed to set <flag> to <value>"
417+
// - "error: ..." messages related to flags
418+
419+
// Build a map of flag names to their results
420+
flagResults := make(map[string]string)
421+
422+
// Regex patterns to match success and error messages
423+
// Flag names can contain hyphens, so use [\w-]+ instead of \S+
424+
successPattern := regexp.MustCompile(`set ([\w-]+) to ([^,]+)`)
425+
errorPattern := regexp.MustCompile(`failed to set ([\w-]+) to ([^,]+)`)
426+
427+
// Parse stderr line by line
428+
lines := strings.Split(stderrOutput, "\n")
429+
for _, line := range lines {
430+
// Check for success messages - use FindAllStringSubmatch to find all matches on the line
431+
successMatches := successPattern.FindAllStringSubmatch(line, -1)
432+
for _, matches := range successMatches {
433+
if len(matches) >= 3 {
434+
flagName := matches[1]
435+
value := strings.TrimSpace(matches[2])
436+
flagResults[flagName] = fmt.Sprintf("✓ Set %s to %s", flagName, value)
437+
}
438+
}
439+
440+
// Check for error messages - use FindAllStringSubmatch to find all matches on the line
441+
errorMatches := errorPattern.FindAllStringSubmatch(line, -1)
442+
for _, matches := range errorMatches {
443+
if len(matches) >= 3 {
444+
flagName := matches[1]
445+
value := strings.TrimSpace(matches[2])
446+
flagResults[flagName] = fmt.Sprintf("✗ Failed to set %s to %s", flagName, value)
447+
}
448+
}
449+
}
450+
451+
// Present results in the order of flagValues
452+
if len(flagValues) > 0 {
453+
fmt.Println("\nConfiguration Results:")
454+
for _, fv := range flagValues {
455+
if result, found := flagResults[fv.flagName]; found {
456+
fmt.Printf(" %s\n", result)
457+
} else {
458+
// If no explicit success or error was found, show unknown status
459+
fmt.Printf(" ? %s: status unknown\n", fv.flagName)
460+
}
461+
}
462+
fmt.Println()
463+
}
464+
}

cmd/config/restore_test.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ package config
44
// SPDX-License-Identifier: BSD-3-Clause
55

66
import (
7+
"bytes"
8+
"io"
79
"os"
10+
"strings"
811
"testing"
912

1013
"github.com/stretchr/testify/assert"
@@ -190,3 +193,180 @@ func TestParseEnableDisableOrOption(t *testing.T) {
190193
})
191194
}
192195
}
196+
197+
func TestParseAndPresentResults(t *testing.T) {
198+
tests := []struct {
199+
name string
200+
stderrOutput string
201+
flagValues []flagValue
202+
expectedOutput []string // lines we expect to see in output
203+
}{
204+
{
205+
name: "Example from function header comment",
206+
stderrOutput: "configuration update complete: set gov to powersave, set c1-demotion to disable, set tdp to 350, set c6 to enable, set epb to 0, set core-max to 3.2, set cores to 86, set elc to default, failed to set pref-l2hw to enable, set pref-dcuhw to enable, set pref-llc to disable, set pref-aop to enable, set pref-l2adj to enable, set uncore-max-compute to 2.2, failed to set llc to 336, set pref-dcunp to enable, set pref-homeless to enable, set pref-amp to enable, set pref-dcuip to enable, set pref-llcpp to enable, set uncore-max-io to 2.5, set uncore-min-compute to 0.8, set uncore-min-io to 0.8",
207+
flagValues: []flagValue{
208+
{flagName: "cores", value: "86"},
209+
{flagName: "llc", value: "336"},
210+
{flagName: "tdp", value: "350"},
211+
{flagName: "core-max", value: "3.2"},
212+
{flagName: "uncore-max-compute", value: "2.2"},
213+
{flagName: "uncore-min-compute", value: "0.8"},
214+
{flagName: "uncore-max-io", value: "2.5"},
215+
{flagName: "uncore-min-io", value: "0.8"},
216+
{flagName: "epb", value: "0"},
217+
{flagName: "gov", value: "powersave"},
218+
{flagName: "elc", value: "default"},
219+
{flagName: "pref-l2hw", value: "enable"},
220+
{flagName: "pref-l2adj", value: "enable"},
221+
{flagName: "pref-dcuhw", value: "enable"},
222+
{flagName: "pref-dcuip", value: "enable"},
223+
{flagName: "pref-dcunp", value: "enable"},
224+
{flagName: "pref-amp", value: "enable"},
225+
{flagName: "pref-llcpp", value: "enable"},
226+
{flagName: "pref-aop", value: "enable"},
227+
{flagName: "pref-homeless", value: "enable"},
228+
{flagName: "pref-llc", value: "disable"},
229+
{flagName: "c6", value: "enable"},
230+
{flagName: "c1-demotion", value: "disable"},
231+
},
232+
expectedOutput: []string{
233+
"✓ Set cores to 86",
234+
"✗ Failed to set llc to 336",
235+
"✓ Set tdp to 350",
236+
"✓ Set core-max to 3.2",
237+
"✓ Set uncore-max-compute to 2.2",
238+
"✓ Set uncore-min-compute to 0.8",
239+
"✓ Set uncore-max-io to 2.5",
240+
"✓ Set uncore-min-io to 0.8",
241+
"✓ Set epb to 0",
242+
"✓ Set gov to powersave",
243+
"✓ Set elc to default",
244+
"✗ Failed to set pref-l2hw to enable",
245+
"✓ Set pref-l2adj to enable",
246+
"✓ Set pref-dcuhw to enable",
247+
"✓ Set pref-dcuip to enable",
248+
"✓ Set pref-dcunp to enable",
249+
"✓ Set pref-amp to enable",
250+
"✓ Set pref-llcpp to enable",
251+
"✓ Set pref-aop to enable",
252+
"✓ Set pref-homeless to enable",
253+
"✓ Set pref-llc to disable",
254+
"✓ Set c6 to enable",
255+
"✓ Set c1-demotion to disable",
256+
},
257+
},
258+
{
259+
name: "Empty stderr output",
260+
stderrOutput: "",
261+
flagValues: []flagValue{
262+
{flagName: "cores", value: "86"},
263+
},
264+
expectedOutput: []string{}, // nothing should be printed
265+
},
266+
{
267+
name: "Mixed success and error messages on separate lines",
268+
stderrOutput: "gnr ⣾ preparing target\n" +
269+
"gnr ⣽ configuration update complete: set cores to 86, failed to set llc to 336, set tdp to 350\n",
270+
flagValues: []flagValue{
271+
{flagName: "cores", value: "86"},
272+
{flagName: "llc", value: "336"},
273+
{flagName: "tdp", value: "350"},
274+
},
275+
expectedOutput: []string{
276+
"✓ Set cores to 86",
277+
"✗ Failed to set llc to 336",
278+
"✓ Set tdp to 350",
279+
},
280+
},
281+
{
282+
name: "Flag name with multiple hyphens",
283+
stderrOutput: "set uncore-max-compute to 2.2, set uncore-min-io to 0.8",
284+
flagValues: []flagValue{
285+
{flagName: "uncore-max-compute", value: "2.2"},
286+
{flagName: "uncore-min-io", value: "0.8"},
287+
},
288+
expectedOutput: []string{
289+
"✓ Set uncore-max-compute to 2.2",
290+
"✓ Set uncore-min-io to 0.8",
291+
},
292+
},
293+
{
294+
name: "No matching flags in output",
295+
stderrOutput: "some other message without flag updates",
296+
flagValues: []flagValue{
297+
{flagName: "cores", value: "86"},
298+
},
299+
expectedOutput: []string{
300+
"? cores: status unknown",
301+
},
302+
},
303+
{
304+
name: "Flag with underscore and numbers",
305+
stderrOutput: "set pref_test123 to enable, failed to set flag_456 to disable",
306+
flagValues: []flagValue{
307+
{flagName: "pref_test123", value: "enable"},
308+
{flagName: "flag_456", value: "disable"},
309+
},
310+
expectedOutput: []string{
311+
"✓ Set pref_test123 to enable",
312+
"✗ Failed to set flag_456 to disable",
313+
},
314+
},
315+
{
316+
name: "Some flags updated, others not mentioned",
317+
stderrOutput: "set cores to 86, set tdp to 350",
318+
flagValues: []flagValue{
319+
{flagName: "cores", value: "86"},
320+
{flagName: "llc", value: "336"},
321+
{flagName: "tdp", value: "350"},
322+
},
323+
expectedOutput: []string{
324+
"✓ Set cores to 86",
325+
"? llc: status unknown",
326+
"✓ Set tdp to 350",
327+
},
328+
},
329+
}
330+
331+
for _, tt := range tests {
332+
t.Run(tt.name, func(t *testing.T) {
333+
// Capture stdout
334+
oldStdout := os.Stdout
335+
r, w, _ := os.Pipe()
336+
os.Stdout = w
337+
338+
// Call the function
339+
parseAndPresentResults(tt.stderrOutput, tt.flagValues)
340+
341+
// Restore stdout
342+
w.Close()
343+
os.Stdout = oldStdout
344+
345+
// Read captured output
346+
var buf bytes.Buffer
347+
io.Copy(&buf, r)
348+
output := buf.String()
349+
350+
// Verify expected output
351+
if len(tt.expectedOutput) == 0 {
352+
// Should be empty or just whitespace
353+
if strings.TrimSpace(output) != "" {
354+
t.Errorf("Expected no output, got: %q", output)
355+
}
356+
return
357+
}
358+
359+
// Check that each expected line is in the output
360+
for _, expected := range tt.expectedOutput {
361+
if !strings.Contains(output, expected) {
362+
t.Errorf("Expected output to contain %q, but it didn't.\nFull output:\n%s", expected, output)
363+
}
364+
}
365+
366+
// Verify the output contains "Configuration Results:" header
367+
if !strings.Contains(output, "Configuration Results:") {
368+
t.Errorf("Expected output to contain 'Configuration Results:' header")
369+
}
370+
})
371+
}
372+
}

0 commit comments

Comments
 (0)