diff --git a/Makefile b/Makefile index 75443f1e..51959648 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,21 @@ -.phony: all +.PHONY: all all: clean build -.phony: test +.PHONY: test test: - @echo "Testing..." + @echo "\n🧪 Testing...\n" go clean -testcache go test ./src/... - @echo "Done." + @echo "\n✅ Done.\n" -.phony: build +.PHONY: build build: - @echo "Building node..." + @echo "\n🛠 Building node...\n" cd src && go build -o ../dist/b7s - @echo "Done." + @echo "\n✅ Done.\n" -.phony: clean +.PHONY: clean clean: - @echo "Cleaning..." + @echo "\n🧹 Cleaning...\n" rm -rf dist - @echo "Done." \ No newline at end of file + @echo "\n✅ Done.\n" diff --git a/src/12D3KooWQrN5U3BApv4JYjE5HyKXFKkRF2U8c5FgK3zMPjzkZTpQ_appDb/OPTIONS-000003 b/src/12D3KooWQrN5U3BApv4JYjE5HyKXFKkRF2U8c5FgK3zMPjzkZTpQ_appDb/OPTIONS-000003 deleted file mode 100644 index 1dd3648f..00000000 --- a/src/12D3KooWQrN5U3BApv4JYjE5HyKXFKkRF2U8c5FgK3zMPjzkZTpQ_appDb/OPTIONS-000003 +++ /dev/null @@ -1,45 +0,0 @@ -[Version] - pebble_version=0.1 - -[Options] - bytes_per_sync=524288 - cache_size=8388608 - cleaner=delete - compaction_debt_concurrency=1073741824 - comparer=leveldb.BytewiseComparator - disable_wal=false - flush_delay_delete_range=0s - flush_delay_range_key=0s - flush_split_bytes=4194304 - format_major_version=1 - l0_compaction_concurrency=10 - l0_compaction_file_threshold=500 - l0_compaction_threshold=4 - l0_stop_writes_threshold=12 - lbase_max_bytes=67108864 - max_concurrent_compactions=1 - max_manifest_file_size=134217728 - max_open_files=1000 - mem_table_size=4194304 - mem_table_stop_writes_threshold=2 - min_deletion_rate=0 - merger=pebble.concatenate - read_compaction_rate=16000 - read_sampling_multiplier=16 - strict_wal_tail=true - table_cache_shards=8 - table_property_collectors=[] - validate_on_ingest=false - wal_dir= - wal_bytes_per_sync=0 - max_writer_concurrency=0 - force_writer_parallelism=false - -[Level "0"] - block_restart_interval=16 - block_size=4096 - compression=Snappy - filter_policy=none - filter_type=table - index_block_size=4096 - target_file_size=2097152 diff --git a/src/controller/controller_test.go b/src/controller/controller_test.go new file mode 100644 index 00000000..a55c8f57 --- /dev/null +++ b/src/controller/controller_test.go @@ -0,0 +1,146 @@ +package controller + +import ( + "context" + "encoding/json" + "fmt" + "os/exec" + "strings" + "testing" + + "github.com/blocklessnetworking/b7s/src/db" + "github.com/blocklessnetworking/b7s/src/enums" + "github.com/blocklessnetworking/b7s/src/memstore" + "github.com/blocklessnetworking/b7s/src/models" +) + +func TestIsFunctionInstalled(t *testing.T) { + + // set up a mock function manifest to store in the database + mockManifest := models.FunctionManifest{ + Function: models.Function{ + ID: "test-function", + Name: "Test Function", + Version: "1.0.0", + Runtime: "go", + }, + Deployment: models.Deployment{ + Cid: "Qmabcdef", + Checksum: "123456789", + Uri: "https://ipfs.io/ipfs/Qmabcdef", + Methods: []models.Methods{ + { + Name: "TestMethod", + Entry: "main.TestMethod", + }, + }, + }, + Runtime: models.Runtime{ + Checksum: "987654321", + Url: "https://ipfs.io/ipfs/Qmzyxwvu", + }, + } + mockManifestBytes, _ := json.Marshal(mockManifest) + + appDb := db.GetDb("/tmp/b7s") + defer db.Close(appDb) + ctx := context.WithValue(context.Background(), "appDb", appDb) + + // Insert a test value into the database + db.Set(ctx, "test_key", string(mockManifestBytes)) + + // Call IsFunctionInstalled + functionManifest, err := IsFunctionInstalled(ctx, "test_key") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Compare the function manifests as strings to account for potential encoding issues + functionManifestBytes, _ := json.Marshal(functionManifest) + expectedManifestBytes, _ := json.Marshal(mockManifest) + if string(functionManifestBytes) != string(expectedManifestBytes) { + t.Errorf("Unexpected function manifest. Got %v, expected %v", functionManifest, mockManifest) + } +} +func TestExecuteFunction(t *testing.T) { + // Create a mock Config value to pass to the context + mockConfig := models.Config{ + Protocol: models.ConfigProtocol{ + Role: enums.RoleWorker, + }, + Node: models.ConfigNode{ + WorkspaceRoot: "/tmp/b7s_tests", + }, + } + ctx := context.WithValue(context.Background(), "config", mockConfig) + testStringValue := "foo" + testString := fmt.Sprintf("echo %s", testStringValue) + // Inject a mock execCommand function + mockExecCommand := func(command string, args ...string) *exec.Cmd { + cs := []string{"-c", testString} + cmd := exec.Command("bash", cs...) + return cmd + } + ctx = context.WithValue(ctx, "execCommand", mockExecCommand) + + // Set up a mock function manifest to store in the database + mockManifest := models.FunctionManifest{ + Function: models.Function{ + ID: "test-function", + Name: "Test Function", + Version: "1.0.0", + Runtime: "go", + }, + Deployment: models.Deployment{ + Cid: "Qmabcdef", + Checksum: "123456789", + Uri: "https://ipfs.io/ipfs/Qmabcdef", + Methods: []models.Methods{ + { + Name: "TestMethod", + Entry: "main.TestMethod", + }, + }, + }, + Runtime: models.Runtime{ + Checksum: "987654321", + Url: "https://ipfs.io/ipfs/Qmzyxwvu", + }, + } + mockManifestBytes, _ := json.Marshal(mockManifest) + + appDb := db.GetDb("/tmp/b7s") + defer db.Close(appDb) + ctx = context.WithValue(ctx, "appDb", appDb) + + // response memstore + executionResponseMemStore := memstore.NewReqRespStore() + ctx = context.WithValue(ctx, "executionResponseMemStore", executionResponseMemStore) + + // Insert the mock function manifest into the database + db.Set(ctx, "test-function", string(mockManifestBytes)) + + // Create a mock RequestExecute value to pass to ExecuteFunction + mockRequest := models.RequestExecute{ + FunctionId: "test-function", + Method: "TestMethod", + Parameters: []models.RequestExecuteParameters{ + { + Name: "param1", + Value: "value1", + }, + }, + Config: models.ExecutionRequestConfig{}, + } + + // Call ExecuteFunction with the mock context and request + response, err := ExecuteFunction(ctx, mockRequest) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Assert that the correct function was called (Worker`Exec`uteFunction in this case) + if strings.Trim(response.Result, "\n") != testStringValue { + t.Errorf("Unexpected response. Got %v, expected %v", response.Result, testStringValue) + } +} diff --git a/src/executor/executor.go b/src/executor/executor.go index 6abb8071..2884fee3 100644 --- a/src/executor/executor.go +++ b/src/executor/executor.go @@ -1,8 +1,10 @@ +// Executor provides functions for running and interacting with functions. package executor import ( "context" "encoding/json" + "fmt" "io/ioutil" "os" "os/exec" @@ -15,6 +17,7 @@ import ( log "github.com/sirupsen/logrus" ) +// prepExecutionManifest creates the execution manifest file for the specified function. func prepExecutionManifest(ctx context.Context, requestId string, request models.RequestExecute, manifest models.FunctionManifest) (string, error) { config := ctx.Value("config").(models.Config) @@ -60,34 +63,42 @@ func queryRuntime(runtimePath string) error { } return nil } - -// executes a shell command to execute a wasm file func Execute(ctx context.Context, request models.RequestExecute, functionManifest models.FunctionManifest) (models.ExecutorResponse, error) { - requestId, _ := uuid.NewRandom() + requestID, _ := uuid.NewRandom() config := ctx.Value("config").(models.Config) - tempFSPath := filepath.Join(config.Node.WorkspaceRoot, "t", requestId.String(), "fs") + tempFSPath := filepath.Join(config.Node.WorkspaceRoot, "t", requestID.String(), "fs") os.MkdirAll(tempFSPath, os.ModePerm) - // check to see if runtime is available - err := queryRuntime(config.Node.RuntimePath) + var execCommand func(string, ...string) *exec.Cmd + if ctxExecCommand, ok := ctx.Value("execCommand").(func(string, ...string) *exec.Cmd); ok { + execCommand = ctxExecCommand + } else { + // Check if the runtime is available. + if err := queryRuntime(config.Node.RuntimePath); err != nil { + return models.ExecutorResponse{ + Code: enums.ResponseCodeError, + RequestId: requestID.String(), + }, err + } + execCommand = exec.Command + } + // Prepare the execution manifest. + runtimeManifestPath, err := prepExecutionManifest(ctx, requestID.String(), request, functionManifest) if err != nil { return models.ExecutorResponse{ Code: enums.ResponseCodeError, - RequestId: requestId.String(), + RequestId: requestID.String(), }, err } - runtimeManifestPath, err := prepExecutionManifest(ctx, requestId.String(), request, functionManifest) - - var executorResponse models.ExecutorResponse - - // check to see if there is any input to pass to the runtime - var input string = "" - var envVars []models.RequestExecuteEnvVars = request.Config.EnvVars - var envVarString string = "" - var envVarKeys string = "" - + // Build the input and environment variable strings. + input := "" + if request.Config.Stdin != nil { + input = *request.Config.Stdin + } + envVars := request.Config.EnvVars + envVarString, envVarKeys := "", "" if len(envVars) > 0 { for _, envVar := range envVars { envVarString += envVar.Name + "=\"" + envVar.Value + "\" " @@ -97,26 +108,25 @@ func Execute(ctx context.Context, request models.RequestExecute, functionManifes envVarKeys = envVarKeys[:len(envVarKeys)-1] } - if request.Config.Stdin != nil { - input = *request.Config.Stdin - } + // Build the command string. + cmd := fmt.Sprintf("echo \"%s\" | %s BLS_LIST_VARS=\"%s\" %s/blockless-cli %s", input, envVarString, envVarKeys, config.Node.RuntimePath, runtimeManifestPath) - cmd := "echo \"" + input + "\" | " + envVarString + " BLS_LIST_VARS=\"" + envVarKeys + "\" " + config.Node.RuntimePath + "/blockless-cli " + runtimeManifestPath - run := exec.Command("bash", "-c", cmd) + // Execute the command. + run := execCommand("bash", "-c", cmd) run.Dir = tempFSPath out, err := run.Output() - if err != nil { - - log.WithFields(log.Fields{ - "err": err, - }).Error("failed to execute request") - - return executorResponse, err + log.WithFields(log.Fields{"err": err}).Error("failed to execute request") + return models.ExecutorResponse{ + Code: enums.ResponseCodeError, + RequestId: requestID.String(), + }, err } + // Store the result in the execution response memory store + executionResponseMemStore := ctx.Value("executionResponseMemStore").(memstore.ReqRespStore) - err = executionResponseMemStore.Set(requestId.String(), &models.MsgExecuteResponse{ + err = executionResponseMemStore.Set(requestID.String(), &models.MsgExecuteResponse{ Type: enums.MsgExecuteResponse, Code: enums.ResponseCodeOk, Result: string(out), @@ -129,11 +139,11 @@ func Execute(ctx context.Context, request models.RequestExecute, functionManifes } log.WithFields(log.Fields{ - "requestId": requestId, + "requestId": requestID, }).Info("function executed") - executorResponse = models.ExecutorResponse{ - RequestId: requestId.String(), + executorResponse := models.ExecutorResponse{ + RequestId: requestID.String(), Code: enums.ResponseCodeOk, Result: string(out), }