Skip to content

Commit 1a69d09

Browse files
committed
[#390] Implement real-time trace on toolchain commands execution
- Extract toolchain command execution functions into a separate file (command_runner.go) - Report command traces line by line, instead of dumping all traces after command completion
1 parent d0cc33d commit 1a69d09

File tree

5 files changed

+170
-71
lines changed

5 files changed

+170
-71
lines changed

src/toolchain/command.go

-49
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ package toolchain
2424

2525
import (
2626
"errors"
27-
"github.com/codeskyblue/go-sh"
28-
"github.com/murex/tcr/report"
2927
"os"
3028
"os/exec"
3129
"path/filepath"
@@ -50,33 +48,6 @@ type (
5048
Path string
5149
Arguments []string
5250
}
53-
54-
// CommandStatus is the result status of a Command execution
55-
CommandStatus string
56-
57-
// CommandResult contains the result from running a Command
58-
// - Status
59-
CommandResult struct {
60-
Status CommandStatus
61-
Output string
62-
}
63-
)
64-
65-
// Failed indicates is a Command failed
66-
func (r CommandResult) Failed() bool {
67-
return r.Status == CommandStatusFail
68-
}
69-
70-
// Passed indicates is a Command passed
71-
func (r CommandResult) Passed() bool {
72-
return r.Status == CommandStatusPass
73-
}
74-
75-
// List of possible values for CommandStatus
76-
const (
77-
CommandStatusPass CommandStatus = "pass"
78-
CommandStatusFail CommandStatus = "fail"
79-
CommandStatusUnknown CommandStatus = "unknown"
8051
)
8152

8253
// List of possible values for OsName
@@ -129,26 +100,6 @@ func (command Command) runsWithArch(archName ArchName) bool {
129100
return false
130101
}
131102

132-
func (command Command) run() (result CommandResult) {
133-
result = CommandResult{Status: CommandStatusUnknown, Output: ""}
134-
report.PostText(command.asCommandLine())
135-
136-
session := sh.NewSession().SetDir(GetWorkDir())
137-
outputBytes, err := session.Command(command.Path, command.Arguments).CombinedOutput()
138-
139-
if err == nil {
140-
result.Status = CommandStatusPass
141-
} else {
142-
result.Status = CommandStatusFail
143-
}
144-
145-
if outputBytes != nil {
146-
result.Output = string(outputBytes)
147-
report.PostText(result.Output)
148-
}
149-
return result
150-
}
151-
152103
func (command Command) check() error {
153104
if err := command.checkPath(); err != nil {
154105
return err

src/toolchain/command_runner.go

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
Copyright (c) 2024 Murex
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.
21+
*/
22+
23+
package toolchain
24+
25+
import (
26+
"bufio"
27+
"github.com/murex/tcr/report"
28+
"os/exec"
29+
)
30+
31+
type (
32+
// CommandRunner is in charge of managing the lifecycle of a command
33+
CommandRunner struct {
34+
command *exec.Cmd
35+
}
36+
37+
// CommandStatus is the result status of a Command execution
38+
CommandStatus string
39+
40+
// CommandResult contains the result from running a Command
41+
// - Status
42+
CommandResult struct {
43+
Status CommandStatus
44+
Output string
45+
}
46+
)
47+
48+
// Failed indicates is a Command failed
49+
func (r CommandResult) Failed() bool {
50+
return r.Status == CommandStatusFail
51+
}
52+
53+
// Passed indicates is a Command passed
54+
func (r CommandResult) Passed() bool {
55+
return r.Status == CommandStatusPass
56+
}
57+
58+
// List of possible values for CommandStatus
59+
const (
60+
CommandStatusPass CommandStatus = "pass"
61+
CommandStatusFail CommandStatus = "fail"
62+
CommandStatusUnknown CommandStatus = "unknown"
63+
)
64+
65+
// commandRunner singleton instance
66+
var commandRunner = getCommandRunner()
67+
68+
func getCommandRunner() *CommandRunner {
69+
return &CommandRunner{
70+
command: nil,
71+
}
72+
}
73+
74+
// Run launches the execution of the provided command
75+
func (r *CommandRunner) Run(cmd *Command) (result CommandResult) {
76+
result = CommandResult{Status: CommandStatusUnknown, Output: ""}
77+
report.PostText(cmd.asCommandLine())
78+
79+
// Prepare the command
80+
r.command = exec.Command(cmd.Path, cmd.Arguments...) //nolint:gosec
81+
r.command.Dir = GetWorkDir()
82+
83+
// Allow simultaneous trace and capture of command's stdout and stderr
84+
outReader, _ := r.command.StdoutPipe()
85+
errReader, _ := r.command.StderrPipe()
86+
doneOut, doneErr := make(chan bool), make(chan bool)
87+
r.reportCommandTrace(bufio.NewScanner(outReader), doneOut)
88+
r.reportCommandTrace(bufio.NewScanner(errReader), doneErr)
89+
90+
// Start the command asynchronously
91+
errStart := r.command.Start()
92+
if errStart != nil {
93+
report.PostError("Failed to run command: ", errStart.Error())
94+
r.command = nil
95+
return result
96+
}
97+
98+
// Wait for the command to finish
99+
errWait := r.command.Wait()
100+
if errWait != nil {
101+
result.Status = CommandStatusFail
102+
} else {
103+
result.Status = CommandStatusPass
104+
}
105+
106+
r.command = nil
107+
return result
108+
}
109+
110+
func (*CommandRunner) reportCommandTrace(scanner *bufio.Scanner, done chan bool) {
111+
go func() {
112+
for scanner.Scan() {
113+
report.PostText(scanner.Text())
114+
}
115+
done <- true
116+
}()
117+
}

src/toolchain/command_runner_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
Copyright (c) 2024 Murex
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.
21+
*/
22+
23+
package toolchain
24+
25+
import (
26+
"fmt"
27+
"github.com/stretchr/testify/assert"
28+
"testing"
29+
)
30+
31+
func Test_command_result_outcome(t *testing.T) {
32+
33+
testFlags := []struct {
34+
status CommandStatus
35+
expectedPassed bool
36+
expectedFailed bool
37+
}{
38+
{"pass", true, false},
39+
{"fail", false, true},
40+
{"unknown", false, false},
41+
}
42+
for _, tt := range testFlags {
43+
t.Run(fmt.Sprint(tt.status, "_status"), func(t *testing.T) {
44+
result := CommandResult{Status: tt.status}
45+
assert.Equal(t, tt.expectedPassed, result.Passed())
46+
assert.Equal(t, tt.expectedFailed, result.Failed())
47+
})
48+
}
49+
}

src/toolchain/command_test.go

-20
Original file line numberDiff line numberDiff line change
@@ -133,23 +133,3 @@ func Test_a_command_arch_cannot_be_empty(t *testing.T) {
133133
func Test_a_valid_command_should_have_path_os_and_arch_non_empty(t *testing.T) {
134134
assert.NoError(t, ACommand(WithPath("some-path"), WithOs("some-os"), WithArch("some-arch")).check())
135135
}
136-
137-
func Test_command_result_outcome(t *testing.T) {
138-
139-
testFlags := []struct {
140-
status CommandStatus
141-
expectedPassed bool
142-
expectedFailed bool
143-
}{
144-
{"pass", true, false},
145-
{"fail", false, true},
146-
{"unknown", false, false},
147-
}
148-
for _, tt := range testFlags {
149-
t.Run(fmt.Sprint(tt.status, "_status"), func(t *testing.T) {
150-
result := CommandResult{Status: tt.status}
151-
assert.Equal(t, tt.expectedPassed, result.Passed())
152-
assert.Equal(t, tt.expectedFailed, result.Failed())
153-
})
154-
}
155-
}

src/toolchain/toolchain.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,14 @@ func (tchn Toolchain) GetTestCommands() []Command {
150150

151151
// RunBuild runs the build with this toolchain
152152
func (tchn Toolchain) RunBuild() CommandResult {
153-
return findCompatibleCommand(tchn.buildCommands).run()
153+
cmd := findCompatibleCommand(tchn.buildCommands)
154+
return commandRunner.Run(cmd)
154155
}
155156

156157
// RunTests runs the tests with this toolchain
157158
func (tchn Toolchain) RunTests() TestCommandResult {
158-
result := findCompatibleCommand(tchn.testCommands).run()
159+
cmd := findCompatibleCommand(tchn.testCommands)
160+
result := commandRunner.Run(cmd)
159161
testStats, _ := tchn.parseTestReport()
160162
return TestCommandResult{result, testStats}
161163
}

0 commit comments

Comments
 (0)