Initial implementation of new runner architecture

This commit is contained in:
akp 2021-12-10 23:49:08 +00:00
parent f5e50b6c6d
commit 9fd93f91a1
No known key found for this signature in database
GPG key ID: AA5726202C8879B7
9 changed files with 269 additions and 168 deletions

View file

@ -0,0 +1,47 @@
from py import Challenge
import time
import json
TASKS_STR = input()
TASKS = json.loads(TASKS_STR)
def send_result(task_id, ok, output, duration):
print(json.dumps({
"task_id": task_id,
"ok": ok,
"output": str(output) if output is not None else "",
"duration": float(duration),
}), flush=True)
for task in TASKS:
taskPart = task["part"]
task_id = task["task_id"]
run = None
if taskPart == 1:
run = lambda: Challenge.one(task["input"])
elif taskPart == 2:
run = lambda: Challenge.two(task["input"])
elif taskPart == 3:
run = lambda: Challenge.vis(task["input"], task["output_dir"])
else:
send_result(task_id, False, "unknown task part", 0)
continue
start_time = time.time()
res = None
err = None
try:
res = run()
except Exception as e:
err = f"{type(e)}: {e}"
end_time = time.time()
running_time = end_time-start_time
if err is not None:
send_result(task_id, False, err, running_time)
else:
send_result(task_id, True, res, running_time)

View file

@ -6,10 +6,8 @@ import (
"path/filepath"
"github.com/alexflint/go-arg"
"github.com/codemicro/adventOfCode/runtime/benchmark"
"github.com/codemicro/adventOfCode/runtime/challenge"
"github.com/codemicro/adventOfCode/runtime/runners"
au "github.com/logrusorgru/aurora"
)
const (
@ -57,7 +55,7 @@ func run() error {
challengeInputString := string(challengeInput)
if args.Benchmark {
return benchmark.Run(selectedChallenge, challengeInputString, args.BenchmarkN)
//return benchmark.Run(selectedChallenge, challengeInputString, args.BenchmarkN)
}
// List and select implementations
@ -66,60 +64,24 @@ func run() error {
return err
}
runner := runners.Available[selectedImplementation](selectedChallenge.Dir)
lookupTable := make(taskLookupTable)
if args.Visualise {
id := "vis"
runner.Queue(&runners.Task{
TaskID: id,
Part: runners.Visualise,
Input: challengeInputString,
OutputDir: ".", // directory the runner is run in, which is the challenge directory
})
lookupTable[id] = func(r *runners.Result) {
fmt.Print(au.Bold("Visualisation: "))
var status string
var followUpText string
if !r.Ok {
status = incompleteLabel
followUpText = "saying \"" + r.Output + "\""
} else {
status = passLabel
}
if followUpText == "" {
followUpText = fmt.Sprintf("in %.4f seconds", r.Duration)
}
fmt.Print(status)
fmt.Println(au.Gray(10, " "+followUpText))
}
} else {
setupTestTasks(challengeInfo, runner, &lookupTable)
if !args.TestOnly {
setupMainTasks(challengeInputString, runner, &lookupTable)
}
}
fmt.Println()
r, cleanupFn := runner.Run()
for roe := range r {
if roe.Error != nil {
return roe.Error
}
// fmt.Println(*roe.Result)
lookupTable[roe.Result.TaskID](roe.Result)
runner := runners.Available[selectedImplementation](selectedChallenge.Dir)
if err := runner.Start(); err != nil {
return err
}
defer func() {
_ = runner.Stop()
_ = runner.Cleanup()
}()
if err := runTests(runner, challengeInfo); err != nil {
return err
}
if cleanupFn != nil {
cleanupFn()
if err := runMainTasks(runner, challengeInputString); err != nil {
return err
}
return nil

View file

@ -5,9 +5,11 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"os/exec"
"strings"
"sync"
"time"
au "github.com/logrusorgru/aurora"
)
@ -68,6 +70,30 @@ func (c *customWriter) GetEntry() ([]byte, error) {
return x, nil
}
func setupBuffers(cmd *exec.Cmd) (io.WriteCloser, error) {
stdoutWriter := &customWriter{}
cmd.Stdout = stdoutWriter
cmd.Stderr = new(bytes.Buffer)
return cmd.StdinPipe()
}
func checkWait(cmd *exec.Cmd) ([]byte, error) {
c := cmd.Stdout.(*customWriter)
for {
e, err := c.GetEntry()
if err == nil {
return e, nil
}
if cmd.ProcessState != nil {
// this is only populated after program exit - we have an issue
return nil, fmt.Errorf("run failed with exit code %d: %s", cmd.ProcessState.ExitCode(), cmd.Stderr.(*bytes.Buffer).String())
}
time.Sleep(time.Millisecond * 10)
}
}
func readResultsFromCommand(cmd *exec.Cmd) chan ResultOrError {
stdoutWriter := &customWriter{}

View file

@ -3,8 +3,8 @@ from py import Challenge
import time
import json
TASKS_STR = input()
TASKS = json.loads(TASKS_STR)
# TASKS_STR = input()
# TASKS = json.loads(TASKS_STR)
def send_result(task_id, ok, output, duration):
print(json.dumps({
@ -14,7 +14,8 @@ def send_result(task_id, ok, output, duration):
"duration": float(duration),
}), flush=True)
for task in TASKS:
while True:
task = json.loads(input())
taskPart = task["part"]
task_id = task["task_id"]

View file

@ -1,10 +1,11 @@
package runners
import (
"bytes"
_ "embed"
"encoding/json"
"fmt"
au "github.com/logrusorgru/aurora"
"io"
"io/ioutil"
"os"
"os/exec"
@ -12,11 +13,16 @@ import (
"strings"
)
const python3Installation = "python3"
const (
python3Installation = "python3"
pythonWrapperFilename = "runtime-wrapper.py"
)
type pythonRunner struct {
dir string
tasks []*Task
dir string
cmd *exec.Cmd
wrapperFilepath string
stdin io.WriteCloser
}
func newPythonRunner(dir string) Runner {
@ -25,56 +31,82 @@ func newPythonRunner(dir string) Runner {
}
}
func (p *pythonRunner) Queue(task *Task) {
p.tasks = append(p.tasks, task)
}
//go:embed interface/python.py
var pythonInterface []byte
func (p *pythonRunner) Run() (chan ResultOrError, func()) {
wrapperFilename := "runtime-wrapper.py"
wrapperFilepath := filepath.Join(p.dir, wrapperFilename)
// Generate interaction data
taskJSON, err := json.Marshal(p.tasks)
if err != nil {
return makeErrorChan(err), nil
}
func (p *pythonRunner) Start() error {
p.wrapperFilepath = filepath.Join(p.dir, pythonWrapperFilename)
// Save interaction code
err = ioutil.WriteFile(wrapperFilepath, pythonInterface, 0644)
if err != nil {
return makeErrorChan(err), nil
if err := ioutil.WriteFile(p.wrapperFilepath, pythonInterface, 0644); err != nil {
return err
}
// Sort out PYTHONPATH
cwd, err := os.Getwd()
if err != nil {
return makeErrorChan(err), nil
return err
}
absDir, err := filepath.Abs(p.dir)
if err != nil {
return makeErrorChan(err), nil
return err
}
pythonPathVar := strings.Join([]string{
filepath.Join(cwd, "lib"),
filepath.Join(absDir, "py"),
filepath.Join(cwd, "lib"), // so we can use aocpy
filepath.Join(absDir, "py"), // so we can import stuff in the challenge directory
}, ":")
fmt.Println("Running...")
p.cmd = exec.Command(python3Installation, "-B", pythonWrapperFilename) // -B prevents .pyc files from being written
p.cmd.Env = append(p.cmd.Env, "PYTHONPATH="+pythonPathVar)
p.cmd.Dir = p.dir
// Run Python and gather output
cmd := exec.Command(python3Installation, "-B", wrapperFilename) // -B prevents .pyc files from being written
cmd.Env = append(cmd.Env, "PYTHONPATH="+pythonPathVar) // this allows the aocpy lib to be imported
cmd.Dir = p.dir
cmd.Stdin = bytes.NewReader(append(taskJSON, '\n'))
return readResultsFromCommand(cmd), func() {
// Remove leftover files
_ = os.Remove(wrapperFilepath)
if stdin, err := setupBuffers(p.cmd); err != nil {
return err
} else {
p.stdin = stdin
}
return p.cmd.Start()
}
func (p *pythonRunner) Stop() error {
if p.cmd == nil || p.cmd.Process == nil {
return nil
}
return p.cmd.Process.Kill()
}
func (p *pythonRunner) Cleanup() error {
if p.wrapperFilepath == "" {
return nil
}
_ = os.Remove(p.wrapperFilepath)
return nil
}
func (p *pythonRunner) Run(task *Task) (*Result, error) {
taskJSON, err := json.Marshal(task)
if err != nil {
return nil, err
}
_, _ = p.stdin.Write(append(taskJSON, '\n'))
res := new(Result)
for {
inp, err := checkWait(p.cmd)
if err != nil {
return nil, err
}
err = json.Unmarshal(inp, res)
if err != nil {
// echo anything that won't parse to stdout (this lets us add debug print statements)
fmt.Printf("[%s] %v\n", au.BrightRed("DBG"), strings.TrimSpace(string(inp)))
} else {
break
}
}
return res, nil
}

View file

@ -24,12 +24,12 @@ type RunnerCreator func(dir string) Runner
var Available = map[string]RunnerCreator{
"py": newPythonRunner,
"go": newGolangRunner,
"nim": newNimRunner,
//"go": newGolangRunner,
//"nim": newNimRunner,
}
var RunnerNames = map[string]string{
"py": "Python",
"go": "Golang",
"nim": "Nim",
//"go": "Golang",
//"nim": "Nim",
}

View file

@ -2,6 +2,8 @@ package main
import (
"fmt"
"strconv"
"strings"
"github.com/codemicro/adventOfCode/runtime/challenge"
"github.com/codemicro/adventOfCode/runtime/runners"
@ -16,86 +18,117 @@ var (
incompleteLabel = au.BgBrightRed("did not complete").String()
)
func setupTestTasks(info *challenge.Info, runner runners.Runner, table *taskLookupTable) {
st := func(part runners.Part, testCases []*challenge.TestCase, runner runners.Runner, table *taskLookupTable) {
for i, testCase := range testCases {
// loop variables change, remember? :P
i := i
testCase := testCase
id := fmt.Sprintf("test.%d.%d", part, i)
runner.Queue(&runners.Task{
TaskID: id,
Part: part,
Input: testCase.Input,
})
(*table)[id] = func(r *runners.Result) {
passed := r.Output == testCase.Expected
fmt.Print(au.Bold(fmt.Sprintf("Test %s: ",
au.BrightBlue(fmt.Sprintf("%d.%d", part, i)),
)))
var status string
var followUpText string
if !r.Ok {
status = incompleteLabel
followUpText = "saying \"" + r.Output + "\""
} else if passed {
status = passLabel
} else {
status = failLabel
}
if followUpText == "" {
followUpText = fmt.Sprintf("in %.4f seconds", r.Duration)
}
fmt.Print(status)
fmt.Println(au.Gray(10, " "+followUpText))
if !passed && r.Ok {
fmt.Printf(" └ Expected %s, got %s\n", au.BrightBlue(testCase.Expected), au.BrightBlue(r.Output))
}
}
}
}
st(runners.PartOne, info.TestCases.One, runner, table)
st(runners.PartTwo, info.TestCases.Two, runner, table)
func makeTestID(part runners.Part, n int) string {
return fmt.Sprintf("test.%d.%d", part, n)
}
func setupMainTasks(input string, runner runners.Runner, table *taskLookupTable) {
func parseTestID(x string) (runners.Part, int) {
y := strings.Split(x, ".")
p, _ := strconv.Atoi(y[1])
n, _ := strconv.Atoi(y[2])
return runners.Part(p), n
}
func makeMainID(part runners.Part) string {
return fmt.Sprintf("main.%d", part)
}
func parseMainID(x string) runners.Part {
y := strings.Split(x, ".")
p, _ := strconv.Atoi(y[1])
return runners.Part(p)
}
func runTests(runner runners.Runner, info *challenge.Info) error {
for i, testCase := range info.TestCases.One {
id := makeTestID(runners.PartOne, i)
result, err := runner.Run(&runners.Task{
TaskID: id,
Part: runners.PartOne,
Input: testCase.Input,
})
if err != nil {
return err
}
handleTestResult(result, testCase)
}
for i, testCase := range info.TestCases.Two {
id := makeTestID(runners.PartTwo, i)
result, err := runner.Run(&runners.Task{
TaskID: id,
Part: runners.PartTwo,
Input: testCase.Input,
})
if err != nil {
return err
}
handleTestResult(result, testCase)
}
return nil
}
func handleTestResult(r *runners.Result, testCase *challenge.TestCase) {
part, n := parseTestID(r.TaskID)
fmt.Print(au.Bold(fmt.Sprintf("Test %s: ",
au.BrightBlue(fmt.Sprintf("%d.%d", part, n)),
)))
passed := r.Output == testCase.Expected
var status string
var followUpText string
if !r.Ok {
status = incompleteLabel
followUpText = "saying \"" + r.Output + "\""
} else if passed {
status = passLabel
} else {
status = failLabel
}
if followUpText == "" {
followUpText = fmt.Sprintf("in %.4f seconds", r.Duration)
}
fmt.Print(status)
fmt.Println(au.Gray(10, " "+followUpText))
if !passed && r.Ok {
fmt.Printf(" └ Expected %s, got %s\n", au.BrightBlue(testCase.Expected), au.BrightBlue(r.Output))
}
}
func runMainTasks(runner runners.Runner, input string) error {
for part := runners.PartOne; part <= runners.PartTwo; part += 1 {
part := part // loop variables need to be copied be used elsewhere to be used in function below, otherwise they
// become rather wrong
id := fmt.Sprintf("main.%d", part)
runner.Queue(&runners.Task{
id := makeMainID(part)
result, err := runner.Run(&runners.Task{
TaskID: id,
Part: part,
Input: input,
})
(*table)[id] = func(r *runners.Result) {
fmt.Print(au.Bold(fmt.Sprintf("Part %d: ", au.Yellow(part))))
if !r.Ok {
fmt.Print(incompleteLabel)
fmt.Println(au.Gray(10, " saying \""+r.Output+"\""))
} else {
fmt.Print(au.BrightBlue(r.Output))
fmt.Println(au.Gray(10, fmt.Sprintf(" in %.4f seconds", r.Duration)))
}
if err != nil {
return err
}
handleMainResult(result)
}
return nil
}
func handleMainResult(r *runners.Result) {
part := parseMainID(r.TaskID)
fmt.Print(au.Bold(fmt.Sprintf("Part %d: ", au.Yellow(part))))
if !r.Ok {
fmt.Print(incompleteLabel)
fmt.Println(au.Gray(10, " saying \""+r.Output+"\""))
} else {
fmt.Print(au.BrightBlue(r.Output))
fmt.Println(au.Gray(10, fmt.Sprintf(" in %.4f seconds", r.Duration)))
}
}