From be0a54a1a6fb2441ea795244659f404aaed76449 Mon Sep 17 00:00:00 2001 From: Maas Lalani Date: Sun, 13 Jun 2021 01:34:21 -0400 Subject: [PATCH] Execute code block for supported languages (bash,go,ruby,python) --- internal/code/code.go | 65 +++++++++++++++++++ internal/code/execute_test.go | 114 ++++++++++++++++++++++++++++++++++ internal/code/languages.go | 33 ++++++++++ 3 files changed, 212 insertions(+) create mode 100644 internal/code/execute_test.go create mode 100644 internal/code/languages.go diff --git a/internal/code/code.go b/internal/code/code.go index c4dbca86..eefd8936 100644 --- a/internal/code/code.go +++ b/internal/code/code.go @@ -2,7 +2,11 @@ package code import ( "errors" + "io/ioutil" + "os" + "os/exec" "regexp" + "time" ) type Block struct { @@ -10,6 +14,12 @@ type Block struct { Language string } +type Result struct { + Out string + ExitCode int + ExecutionTime time.Duration +} + // ?: means non-capture group var re = regexp.MustCompile("(?:```|~~~)(.*)\n(.*)\n(?:```|~~~)") @@ -33,3 +43,58 @@ func Parse(markdown string) (Block, error) { Code: match[2], }, nil } + +const ( + // ExitCodeInternalError represents the exit code in which the code + // executing the code didn't work. + ExitCodeInternalError = -1 +) + +// Execute takes a code.Block and returns the output of the executed code +func Execute(code Block) Result { + // Check supported language + language, ok := Languages[code.Language] + if !ok { + return Result{ + Out: "Error: unsupported language", + ExitCode: ExitCodeInternalError, + } + } + + // Write the code block to a temporary file + f, err := ioutil.TempFile(os.TempDir(), "slides-*."+Languages[code.Language].Extension) + if err != nil { + return Result{ + Out: "Error: could not create file", + ExitCode: ExitCodeInternalError, + } + } + defer os.Remove(f.Name()) + + _, err = f.WriteString(code.Code) + if err != nil { + return Result{ + Out: "Error: could not write to file", + ExitCode: ExitCodeInternalError, + } + } + + cmd := exec.Command(language.Command[0], append(language.Command[1:], f.Name())...) + + // For accuracy of program execution speed, we can't put anything after + // recording the start time or before recording the end time. + start := time.Now() + out, err := cmd.Output() + end := time.Now() + + exitCode := 0 + if err != nil { + exitCode = 1 + } + + return Result{ + Out: string(out), + ExitCode: exitCode, + ExecutionTime: end.Sub(start), + } +} diff --git a/internal/code/execute_test.go b/internal/code/execute_test.go new file mode 100644 index 00000000..f3dddd81 --- /dev/null +++ b/internal/code/execute_test.go @@ -0,0 +1,114 @@ +package code_test + +import ( + "testing" + + "github.com/maaslalani/slides/internal/code" +) + +func TestExecute(t *testing.T) { + tt := []struct { + block code.Block + expected code.Result + }{ + { + block: code.Block{ + Code: `puts "Hello, world!"`, + Language: "ruby", + }, + expected: code.Result{ + Out: "Hello, world!\n", + ExitCode: 0, + }, + }, + { + block: code.Block{ + Code: `puts "Hi, there!"`, + Language: "ruby", + }, + expected: code.Result{ + Out: "Hi, there!\n", + ExitCode: 0, + }, + }, + { + block: code.Block{ + Code: `print "No new line"`, + Language: "ruby", + }, + expected: code.Result{ + Out: "No new line", + ExitCode: 0, + }, + }, + { + block: code.Block{ + Code: ` +package main + +import "fmt" + +func main() { + fmt.Print("Hello, go!") +} + `, + Language: "go", + }, + expected: code.Result{ + Out: "Hello, go!", + ExitCode: 0, + }, + }, + { + block: code.Block{ + Code: `print("Hello, python!")`, + Language: "python", + }, + expected: code.Result{ + Out: "Hello, python!\n", + ExitCode: 0, + }, + }, + { + block: code.Block{ + Code: `echo "Hello, bash!"`, + Language: "bash", + }, + expected: code.Result{ + Out: "Hello, bash!\n", + ExitCode: 0, + }, + }, + { + block: code.Block{ + Code: `Invalid Code`, + Language: "bash", + }, + expected: code.Result{ + Out: "", + ExitCode: 1, + }, + }, + { + block: code.Block{ + Code: `Invalid Code`, + Language: "invalid", + }, + expected: code.Result{ + Out: "Error: unsupported language", + ExitCode: code.ExitCodeInternalError, + }, + }, + } + + for _, tc := range tt { + r := code.Execute(tc.block) + if r.Out != tc.expected.Out { + t.Fatalf("invalid output, got %s, want %s", r.Out, tc.expected.Out) + } + + if r.ExitCode != tc.expected.ExitCode { + t.Fatalf("unexpected exit code, got %d, want %d", r.ExitCode, tc.expected.ExitCode) + } + } +} diff --git a/internal/code/languages.go b/internal/code/languages.go new file mode 100644 index 00000000..4eaa42e8 --- /dev/null +++ b/internal/code/languages.go @@ -0,0 +1,33 @@ +package code + +type Language struct { + Extension string + Command []string +} + +// Supported Languages +const ( + Bash = "bash" + Go = "go" + Ruby = "ruby" + Python = "python" +) + +var Languages = map[string]Language{ + Bash: { + Extension: "sh", + Command: []string{"bash"}, + }, + Go: { + Extension: "go", + Command: []string{"go", "run"}, + }, + Ruby: { + Extension: "rb", + Command: []string{"ruby"}, + }, + Python: { + Extension: "py", + Command: []string{"python"}, + }, +}