mirror of https://github.com/knative/func.git
feat: enable scaffolding for host builds (#1750)
* feat: enable scaffolding in builder * fix a few typos * error text formatting Co-authored-by: Lance Ball <lball@redhat.com> * error text formatting Co-authored-by: Lance Ball <lball@redhat.com> * remove test job stop defer --------- Co-authored-by: Lance Ball <lball@redhat.com>
This commit is contained in:
parent
9632748f19
commit
e5aff92984
|
@ -17,6 +17,8 @@ import (
|
|||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"knative.dev/func/pkg/scaffolding"
|
||||
"knative.dev/func/pkg/utils"
|
||||
)
|
||||
|
||||
|
@ -620,47 +622,11 @@ func (c *Client) Build(ctx context.Context, f Function) (Function, error) {
|
|||
// It also updates the included symlink to function source 'f' to point to
|
||||
// the current function's source.
|
||||
func (c *Client) Scaffold(ctx context.Context, f Function, dest string) (err error) {
|
||||
// First get a reference to the repository containing the scaffolding to use
|
||||
//
|
||||
// TODO: In order to support extensible scaffolding from external repositories,
|
||||
// Retain the repository reference from which a Function was initialized
|
||||
// in order to re-read out its scaffolding later. This can be the locally-
|
||||
// installed repository name or the remote reference URL. There are benefits
|
||||
// and detriments either way. A third option would be to store the
|
||||
// scaffolding locally, but this also has downsides.
|
||||
//
|
||||
// If function creatd from a local repository named:
|
||||
// repo = repoFromURL(f.RepoURL)
|
||||
// If function created from a remote reference:
|
||||
// c.Repositories().Get(f.RepoName)
|
||||
// If function not created from an external repository:
|
||||
repo, err := c.Repositories().Get(DefaultRepositoryName)
|
||||
repo, err := NewRepository("", "") // default (embedded) repository
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Detect the method signature
|
||||
s, err := functionSignature(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Write Scaffolding from the Repository into the destination
|
||||
if err = repo.WriteScaffolding(ctx, f, s, dest); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Replace the 'f' link of the scaffolding (which is now incorrect) to
|
||||
// link to the function's root.
|
||||
src, err := filepath.Rel(dest, f.Root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error determining relative path to function source %w", err)
|
||||
}
|
||||
_ = os.Remove(filepath.Join(dest, "f"))
|
||||
if err = os.Symlink(src, filepath.Join(dest, "f")); err != nil {
|
||||
return fmt.Errorf("error linking scaffolding to function source %w", err)
|
||||
}
|
||||
return
|
||||
return scaffolding.Write(dest, f.Root, f.Runtime, f.Invoke, repo.FS())
|
||||
}
|
||||
|
||||
// printBuildActivity is a helper for ensuring the user gets feedback from
|
||||
|
|
|
@ -1757,3 +1757,66 @@ func TestClient_CreateMigration(t *testing.T) {
|
|||
t.Fatal("freshly created function should have the latest migration")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClient_RunReadiness ensures that the run task awaits a ready response
|
||||
// from the job before returning.
|
||||
func TestClient_RunReadiness(t *testing.T) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root, cleanup := Mktemp(t)
|
||||
defer cleanup()
|
||||
|
||||
client := fn.New(fn.WithBuilder(oci.NewBuilder("", true)), fn.WithVerbose(true))
|
||||
|
||||
// Initialize
|
||||
f, err := client.Init(fn.Function{Root: root, Runtime: "go", Registry: TestRegistry})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Replace the implementation with the test implementation which will
|
||||
// return a non-200 response for the first few seconds. This confirms
|
||||
// the client is waiting and retrying.
|
||||
// TODO: we need an init option which skips writing example source-code.
|
||||
_ = os.Remove(filepath.Join(root, "function.go"))
|
||||
_ = os.Remove(filepath.Join(root, "function_test.go"))
|
||||
_ = os.Remove(filepath.Join(root, "handle.go"))
|
||||
_ = os.Remove(filepath.Join(root, "handle_test.go"))
|
||||
src, err := os.Open(filepath.Join(cwd, "testdata", "testClientRunReadiness", "f.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dst, err := os.Create(filepath.Join(root, "f.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err = io.Copy(dst, src); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src.Close()
|
||||
dst.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Build
|
||||
if f, err = client.Build(ctx, f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run
|
||||
// The function returns a non-200 from its readiness handler at first.
|
||||
// Since we already confirmed in another test that a timeout awaiting a
|
||||
// 200 response from this endpoint does indeed fail the run task, this
|
||||
// delayed 200 confirms there is a retry in place.
|
||||
job, err := client.Run(ctx, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := job.Stop(); err != nil {
|
||||
t.Fatalf("err on job stop. %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,9 @@ func NewJob(f Function, host, port string, errs chan error, onStop func() error,
|
|||
if j.Errors == nil {
|
||||
j.Errors = make(chan error, 1)
|
||||
}
|
||||
if j.onStop == nil {
|
||||
j.onStop = func() error { return nil }
|
||||
}
|
||||
if err = cleanupJobDirs(j); err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package functions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
@ -156,6 +155,11 @@ func NewRepository(name, uri string) (r Repository, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// FS returns the underlying filesystem of this repository.
|
||||
func (r Repository) FS() filesystem.Filesystem {
|
||||
return r.fs
|
||||
}
|
||||
|
||||
// filesystemFromURI returns a filesystem from the data located at the
|
||||
// given URI. If URI is not provided, indicates the embedded repo should
|
||||
// be loaded. URI can be a remote git repository (http:// https:// etc.),
|
||||
|
@ -525,30 +529,6 @@ func (r *Repository) Write(dest string) (err error) {
|
|||
return filesystem.CopyFromFS(".", dest, fs)
|
||||
}
|
||||
|
||||
// WriteScaffolding code to the given path.
|
||||
//
|
||||
// Scaffolding is a language-level operation which first detects the method
|
||||
// signature used by the function's source code and then writes the
|
||||
// appropriate scaffolding.
|
||||
//
|
||||
// NOTE: Scaffoding is not per-template, because a template is merely an
|
||||
// example starting point for a Function implementation and should have no
|
||||
// bearing on the shape that function can eventually take. The language,
|
||||
// and optionally invocation hint (For cloudevents) are used for this. For
|
||||
// example, there can be multiple templates which exemplify a given method
|
||||
// signature, and the implementation can be switched at any time by the author.
|
||||
// Language, by contrast, is fixed at time of initialization.
|
||||
func (r *Repository) WriteScaffolding(ctx context.Context, f Function, s Signature, dest string) error {
|
||||
if r.fs == nil {
|
||||
return errors.New("repository has no filesystem")
|
||||
}
|
||||
path := fmt.Sprintf("%v/scaffolding/%v", f.Runtime, s.String()) // fs uses / on all OSs
|
||||
if _, err := r.fs.Stat(path); err != nil {
|
||||
return fmt.Errorf("no scaffolding found for '%v' signature '%v'. %v.", f.Runtime, s, err)
|
||||
}
|
||||
return filesystem.CopyFromFS(path, dest, r.fs)
|
||||
}
|
||||
|
||||
// URL attempts to read the remote git origin URL of the repository. Best
|
||||
// effort; returns empty string if the repository is not a git repo or the repo
|
||||
// has been mutated beyond recognition on disk (ex: removing the origin remote)
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
package functions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Signature int
|
||||
|
||||
const (
|
||||
UnknownSignature Signature = iota
|
||||
InstancedHTTP
|
||||
InstancedCloudevent
|
||||
StaticHTTP
|
||||
StaticCloudevent
|
||||
)
|
||||
|
||||
func (s Signature) String() string {
|
||||
return []string{
|
||||
"unknown",
|
||||
"instanced-http",
|
||||
"instanced-cloudevent",
|
||||
"static-http",
|
||||
"static-cloudevent",
|
||||
}[s]
|
||||
}
|
||||
|
||||
var signatureMap = map[bool]map[string]Signature{
|
||||
true: {
|
||||
"http": InstancedHTTP,
|
||||
"cloudevent": InstancedCloudevent},
|
||||
false: {
|
||||
"http": StaticHTTP,
|
||||
"cloudevent": StaticCloudevent},
|
||||
}
|
||||
|
||||
func signature(instanced bool, invoke string) Signature {
|
||||
if invoke == "" {
|
||||
invoke = "http"
|
||||
}
|
||||
s, ok := signatureMap[instanced][invoke]
|
||||
if !ok {
|
||||
return UnknownSignature
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// detectors check for the existence of certain method signatures in the
|
||||
// source code at the given root.
|
||||
type detector interface {
|
||||
Detect(dir string) (static, instanced bool, err error)
|
||||
}
|
||||
|
||||
// functionSignature returns the signature implemented by the given function
|
||||
func functionSignature(f Function) (s Signature, err error) {
|
||||
d, err := detectorFor(f.Runtime)
|
||||
if err != nil {
|
||||
return UnknownSignature, err
|
||||
}
|
||||
static, instanced, err := d.Detect(f.Root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Function must implement either a static handler or the instanced handler
|
||||
// but not both.
|
||||
if static && instanced {
|
||||
return s, fmt.Errorf("function may not implement both the static and instanced method signatures simultaneously")
|
||||
} else if !static && !instanced {
|
||||
return s, fmt.Errorf("function does not appear to implement any known method signatures")
|
||||
} else if instanced {
|
||||
return signature(true, f.Invoke), nil
|
||||
} else {
|
||||
return signature(false, f.Invoke), nil
|
||||
}
|
||||
}
|
||||
|
||||
// detectorFor runtime returns a signature detector for a given runtime
|
||||
func detectorFor(runtime string) (detector, error) {
|
||||
switch runtime {
|
||||
case "go":
|
||||
return &goDetector{}, nil
|
||||
case "python":
|
||||
return &pythonDetector{}, nil
|
||||
case "rust":
|
||||
return nil, errors.New("the Rust signature detector is not yet available")
|
||||
case "node":
|
||||
return nil, errors.New("the Node.js signature detector is not yet available")
|
||||
case "quarkus":
|
||||
return nil, errors.New("the TypeScript signature detector is not yet available")
|
||||
default:
|
||||
return nil, fmt.Errorf("unable to detect the signature of the unrecognized runtime language %q", runtime)
|
||||
}
|
||||
}
|
||||
|
||||
// GO
|
||||
|
||||
type goDetector struct{}
|
||||
|
||||
func (d goDetector) Detect(dir string) (static, instanced bool, err error) {
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("signature detector encountered an error when scanning the function's source code %w", err)
|
||||
return
|
||||
}
|
||||
for _, file := range files {
|
||||
filename := filepath.Join(dir, file.Name())
|
||||
if file.IsDir() || !strings.HasSuffix(filename, ".go") {
|
||||
continue
|
||||
}
|
||||
if d.hasFunctionDeclaration(filename, "New") {
|
||||
instanced = true
|
||||
}
|
||||
if d.hasFunctionDeclaration(filename, "Handle") {
|
||||
static = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d goDetector) hasFunctionDeclaration(filename, function string) bool {
|
||||
astFile, err := parser.ParseFile(token.NewFileSet(), filename, nil, parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, decl := range astFile.Decls {
|
||||
if funcDecl, ok := decl.(*ast.FuncDecl); ok {
|
||||
if funcDecl.Name.Name == function {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PYTHON
|
||||
|
||||
type pythonDetector struct{}
|
||||
|
||||
func (d pythonDetector) Detect(dir string) (bool, bool, error) {
|
||||
return false, false, errors.New("the Python method signature detector is not yet available.")
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package f
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type F struct {
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
func New() *F {
|
||||
return &F{time.Now()}
|
||||
}
|
||||
|
||||
func (f *F) Handle(_ context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("Request received")
|
||||
fmt.Fprintf(w, "Request received\n")
|
||||
}
|
||||
|
||||
func (f *F) Ready(ctx context.Context) (bool, error) {
|
||||
// Emulate a function which does not start immediately
|
||||
if time.Now().After(f.Created.Add(600 * time.Millisecond)) {
|
||||
return true, nil
|
||||
}
|
||||
return false, errors.New("still starting up")
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module function
|
||||
|
||||
go 1.17
|
|
@ -12,6 +12,7 @@ import (
|
|||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
|
||||
fn "knative.dev/func/pkg/functions"
|
||||
"knative.dev/func/pkg/scaffolding"
|
||||
)
|
||||
|
||||
var path = filepath.Join
|
||||
|
@ -33,25 +34,18 @@ var defaultIgnored = []string{ // TODO: implement and use .funcignore
|
|||
".gitignore",
|
||||
}
|
||||
|
||||
// BuildErr indicates a build error occurred.
|
||||
type BuildErr struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e BuildErr) Error() string {
|
||||
return fmt.Sprintf("error performing host build. %v", e.Err)
|
||||
}
|
||||
|
||||
// Builder which creates an OCI-compliant multi-arch (index) container from
|
||||
// the function at path.
|
||||
type Builder struct {
|
||||
name string
|
||||
verbose bool
|
||||
|
||||
tester *testHelper
|
||||
}
|
||||
|
||||
// NewBuilder creates a builder instance.
|
||||
func NewBuilder(name string, verbose bool) *Builder {
|
||||
return &Builder{name, verbose}
|
||||
return &Builder{name, verbose, nil}
|
||||
}
|
||||
|
||||
// Build an OCI-compliant Mult-arch (v1.ImageIndex) container on disk
|
||||
|
@ -63,52 +57,60 @@ func NewBuilder(name string, verbose bool) *Builder {
|
|||
//
|
||||
// .func/builds/last
|
||||
func (b *Builder) Build(ctx context.Context, f fn.Function) (err error) {
|
||||
cfg := &buildConfig{ctx, f, time.Now(), b.verbose, ""}
|
||||
cfg := &buildConfig{ctx, b.name, f, time.Now(), b.verbose, "", b.tester, defaultPlatforms}
|
||||
|
||||
if err = setup(cfg); err != nil { // create directories and links
|
||||
return
|
||||
}
|
||||
defer teardown(cfg)
|
||||
|
||||
//TODO: Use scaffold package when merged:
|
||||
/*
|
||||
if err = scaffolding.Scaffold(ctx, f, cfg.buildDir()); err != nil {
|
||||
return
|
||||
}
|
||||
*/
|
||||
// IN the meantime, use an airball mainfile
|
||||
data := `
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main () {
|
||||
fmt.Println("Hello, world!")
|
||||
}
|
||||
`
|
||||
if err = os.WriteFile(path(cfg.buildDir(), "main.go"), []byte(data), 0664); err != nil {
|
||||
// Load the embedded repository
|
||||
repo, err := fn.NewRepository("", "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Write out the scaffolding
|
||||
err = scaffolding.Write(cfg.buildDir(), f.Root, f.Runtime, f.Invoke, repo.FS())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create an OCI container from the scaffolded function
|
||||
if err = containerize(cfg); err != nil {
|
||||
return
|
||||
}
|
||||
return updateLastLink(cfg)
|
||||
|
||||
if err = updateLastLink(cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: communicating build completeness throgh returning without error
|
||||
// relies on the implicit availability of the OIC image in this process'
|
||||
// build directory. Would be better to have a formal build result object
|
||||
// which includes a general struct which can be used by all builders to
|
||||
// communicate to the pusher where the image can be found.
|
||||
// Tests, however, can use a simple channel:
|
||||
if cfg.tester != nil && cfg.tester.notifyDone {
|
||||
if cfg.verbose {
|
||||
fmt.Println("tester configured to notify on done. Sending to unbuffered doneCh")
|
||||
}
|
||||
cfg.tester.doneCh <- true
|
||||
fmt.Println("send to doneCh complete")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// buildConfig contains various settings for a single build
|
||||
type buildConfig struct {
|
||||
ctx context.Context // build context
|
||||
f fn.Function // Function being built
|
||||
t time.Time // Timestamp for this build
|
||||
verbose bool // verbose logging
|
||||
h string // hash cache (use .hash() accessor)
|
||||
ctx context.Context // build context
|
||||
name string
|
||||
f fn.Function // Function being built
|
||||
t time.Time // Timestamp for this build
|
||||
verbose bool // verbose logging
|
||||
h string // hash cache (use .hash() accessor)
|
||||
tester *testHelper
|
||||
platforms []v1.Platform
|
||||
}
|
||||
|
||||
func (c *buildConfig) hash() string {
|
||||
|
@ -151,7 +153,7 @@ func (c *buildConfig) blobsDir() string {
|
|||
func setup(cfg *buildConfig) (err error) {
|
||||
// error if already in progress
|
||||
if isActive(cfg, cfg.buildDir()) {
|
||||
return BuildErr{fmt.Errorf("Build directory already exists for this version hash and is associated with an active PID. Is a build already in progress? %v", cfg.buildDir())}
|
||||
return ErrBuildInProgress{cfg.buildDir()}
|
||||
}
|
||||
|
||||
// create build files directory
|
||||
|
@ -181,10 +183,6 @@ func setup(cfg *buildConfig) (err error) {
|
|||
}
|
||||
|
||||
func teardown(cfg *buildConfig) {
|
||||
// remove the pid link for the current process indicating the build is
|
||||
// no longer in progress.
|
||||
_ = os.RemoveAll(cfg.pidLink())
|
||||
|
||||
// remove pid links for processes which no longer exist.
|
||||
dd, _ := os.ReadDir(cfg.pidsDir())
|
||||
for _, d := range dd {
|
||||
|
@ -269,3 +267,22 @@ func updateLastLink(cfg *buildConfig) error {
|
|||
_ = os.RemoveAll(cfg.lastLink())
|
||||
return os.Symlink(cfg.buildDir(), cfg.lastLink())
|
||||
}
|
||||
|
||||
type testHelper struct {
|
||||
emulateSlowBuild bool
|
||||
continueCh chan any
|
||||
|
||||
notifyDone bool
|
||||
doneCh chan any
|
||||
|
||||
notifyPaused bool
|
||||
pausedCh chan any
|
||||
}
|
||||
|
||||
func newTestHelper() *testHelper {
|
||||
return &testHelper{
|
||||
continueCh: make(chan any),
|
||||
doneCh: make(chan any),
|
||||
pausedCh: make(chan any),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package oci
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
@ -35,6 +37,43 @@ func TestBuilder(t *testing.T) {
|
|||
validateOCI(last, t)
|
||||
}
|
||||
|
||||
// TestBuilder_Concurrency
|
||||
func TestBuilder_Concurrency(t *testing.T) {
|
||||
root, done := Mktemp(t)
|
||||
defer done()
|
||||
|
||||
client := fn.New()
|
||||
|
||||
f, err := client.Init(fn.Function{Root: root, Runtime: "go"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start a build which pauses such that we can start a second.
|
||||
builder1 := NewBuilder("builder1", true)
|
||||
builder1.tester = newTestHelper()
|
||||
builder1.tester.emulateSlowBuild = true
|
||||
builder1.tester.notifyPaused = true
|
||||
builder1.tester.notifyDone = true
|
||||
go func() {
|
||||
if err := builder1.Build(context.Background(), f); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "test build error %v", err)
|
||||
}
|
||||
}()
|
||||
<-builder1.tester.pausedCh // wait until it is paused
|
||||
|
||||
builder2 := NewBuilder("builder2", true)
|
||||
go func() {
|
||||
err = builder2.Build(context.Background(), f)
|
||||
if !errors.As(err, &ErrBuildInProgress{}) {
|
||||
fmt.Fprintf(os.Stderr, "test build error %v", err)
|
||||
|
||||
}
|
||||
}()
|
||||
builder1.tester.continueCh <- true // release the paused first builder
|
||||
<-builder1.tester.doneCh // wait for it to be done
|
||||
}
|
||||
|
||||
// ImageIndex represents the structure of an OCI Image Index.
|
||||
type ImageIndex struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
|
|
|
@ -21,7 +21,6 @@ type goLayerBuilder struct{}
|
|||
// the statically linked binary in a tarred layer and return the Descriptor
|
||||
// and Layer metadata.
|
||||
func (c goLayerBuilder) Build(cfg *buildConfig, p v1.Platform) (desc v1.Descriptor, layer v1.Layer, err error) {
|
||||
|
||||
// Executable
|
||||
exe, err := goBuild(cfg, p) // Compile binary returning its path
|
||||
if err != nil {
|
||||
|
@ -55,6 +54,10 @@ func (c goLayerBuilder) Build(cfg *buildConfig, p v1.Platform) (desc v1.Descript
|
|||
}
|
||||
|
||||
func goBuild(cfg *buildConfig, p v1.Platform) (binPath string, err error) {
|
||||
if cfg.tester != nil && cfg.tester.emulateSlowBuild {
|
||||
pauseBuildUntilReleased(cfg, p)
|
||||
}
|
||||
|
||||
gobin, args, outpath, err := goBuildCmd(p, cfg)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -74,6 +77,35 @@ func goBuild(cfg *buildConfig, p v1.Platform) (binPath string, err error) {
|
|||
return outpath, cmd.Run()
|
||||
}
|
||||
|
||||
func isFirstBuild(cfg *buildConfig, current v1.Platform) bool {
|
||||
first := cfg.platforms[0]
|
||||
return current.OS == first.OS &&
|
||||
current.Architecture == first.Architecture &&
|
||||
current.Variant == first.Variant
|
||||
|
||||
}
|
||||
|
||||
func pauseBuildUntilReleased(cfg *buildConfig, p v1.Platform) {
|
||||
if cfg.verbose {
|
||||
fmt.Println("test set to emulate slow build. checking if this build should be paused")
|
||||
}
|
||||
if !isFirstBuild(cfg, p) {
|
||||
if cfg.verbose {
|
||||
fmt.Println("not first build. will not pause")
|
||||
}
|
||||
return
|
||||
}
|
||||
if cfg.verbose {
|
||||
fmt.Println("this is the first build: pausing awaiting release via cfg.tester.continueCh")
|
||||
}
|
||||
fmt.Printf("testing slow builds. %v paused\n", cfg.name)
|
||||
if cfg.tester.notifyPaused {
|
||||
cfg.tester.pausedCh <- true
|
||||
}
|
||||
<-cfg.tester.continueCh
|
||||
fmt.Printf("continuing build\n")
|
||||
}
|
||||
|
||||
func goBuildCmd(p v1.Platform, cfg *buildConfig) (gobin string, args []string, outpath string, err error) {
|
||||
/* TODO: Use Build Command override from the function if provided
|
||||
* A future PR will include the ability to specify a
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package oci
|
||||
|
||||
import "fmt"
|
||||
|
||||
// BuildErr indicates a general build error occurred.
|
||||
type BuildErr struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e BuildErr) Error() string {
|
||||
return fmt.Sprintf("error performing host build. %v", e.Err)
|
||||
}
|
||||
|
||||
type ErrBuildInProgress struct {
|
||||
Dir string
|
||||
}
|
||||
|
||||
func (e ErrBuildInProgress) Error() string {
|
||||
return fmt.Sprintf("a build for this function is associated with an active PID appears to be already in progress %v", e.Dir)
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package scaffolding
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// detector of method signatures. Each instance is for a given runtime.
|
||||
type detector interface {
|
||||
Detect(dir string) (static, instanced bool, err error)
|
||||
}
|
||||
|
||||
// newDetector returns a deector instance for the given runtime.
|
||||
func newDetector(runtime string) (detector, error) {
|
||||
switch runtime {
|
||||
case "go":
|
||||
return &goDetector{}, nil
|
||||
case "python":
|
||||
return &pythonDetector{}, nil
|
||||
case "rust":
|
||||
return nil, ErrDetectorNotImplemented{runtime}
|
||||
case "node":
|
||||
return nil, ErrDetectorNotImplemented{runtime}
|
||||
case "typescript":
|
||||
return nil, ErrDetectorNotImplemented{runtime}
|
||||
case "quarkus":
|
||||
return nil, ErrDetectorNotImplemented{runtime}
|
||||
case "java":
|
||||
return nil, ErrDetectorNotImplemented{runtime}
|
||||
default:
|
||||
return nil, ErrRuntimeNotRecognized{runtime}
|
||||
}
|
||||
}
|
||||
|
||||
// GO
|
||||
|
||||
type goDetector struct{}
|
||||
|
||||
func (d goDetector) Detect(dir string) (static, instanced bool, err error) {
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return static, instanced, fmt.Errorf("signature detector encountered an error when scanning the function's source code. %w", err)
|
||||
}
|
||||
for _, file := range files {
|
||||
filename := filepath.Join(dir, file.Name())
|
||||
if file.IsDir() || !strings.HasSuffix(filename, ".go") {
|
||||
continue
|
||||
}
|
||||
if d.hasFunctionDeclaration(filename, "New") {
|
||||
instanced = true
|
||||
}
|
||||
if d.hasFunctionDeclaration(filename, "Handle") {
|
||||
static = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d goDetector) hasFunctionDeclaration(filename, function string) bool {
|
||||
astFile, err := parser.ParseFile(token.NewFileSet(), filename, nil, parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, decl := range astFile.Decls {
|
||||
if funcDecl, ok := decl.(*ast.FuncDecl); ok {
|
||||
// Name matches and it has no reciver. I.e. a package level function
|
||||
if funcDecl.Name.Name == function && funcDecl.Recv == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PYTHON
|
||||
|
||||
type pythonDetector struct{}
|
||||
|
||||
func (d pythonDetector) Detect(dir string) (bool, bool, error) {
|
||||
return false, false, errors.New("the Python method signature detector is not yet available")
|
||||
}
|
||||
|
||||
// TODO: Other Detectors
|
|
@ -1,7 +1,4 @@
|
|||
//go:build !integration
|
||||
// +build !integration
|
||||
|
||||
package functions
|
||||
package scaffolding
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
@ -12,31 +9,6 @@ import (
|
|||
. "knative.dev/func/pkg/testing"
|
||||
)
|
||||
|
||||
// TestSignature_Map ensures via spot-checking that the mappings for the
|
||||
// different method signature constants are correctly associated to their
|
||||
// string representation, the boolean indicator of instanced, and the
|
||||
// invocation hint as defined on the function; and this association is
|
||||
// traversable via the `signature` method.
|
||||
func TestSignature_Map(t *testing.T) {
|
||||
instanced := false
|
||||
invocation := "http"
|
||||
expectedName := "static-http"
|
||||
expectedSig := StaticHTTP
|
||||
|
||||
s := signature(instanced, invocation)
|
||||
if s != expectedSig {
|
||||
t.Fatal("signature flags incorrectly mapped")
|
||||
}
|
||||
if s.String() != expectedName {
|
||||
t.Fatalf("signature string representation incorrectly mapped. Expected %q got %q", expectedName, s)
|
||||
}
|
||||
|
||||
// ensure that the default for invocation is http
|
||||
if signature(true, "") != InstancedHTTP {
|
||||
t.Fatalf("expected %v, got %v", InstancedHTTP, signature(true, ""))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDetector_Go ensures that the go language detector will correctly
|
||||
// identify the signature to expect of a function's source.
|
||||
func TestDetector_Go(t *testing.T) {
|
||||
|
@ -50,11 +22,11 @@ func TestDetector_Go(t *testing.T) {
|
|||
// scaffolding code needs to be written to get the user to a proper
|
||||
// complile attempt.
|
||||
tests := []struct {
|
||||
Name string // Name of the test
|
||||
Sig Signature // Signature Expected
|
||||
Err error // Error Expected
|
||||
Src string // Source code to check
|
||||
Cfg func(Function) Function // Configure the default function for the test.
|
||||
Name string // Name of the test
|
||||
Sig Signature // Signature Expected
|
||||
Err error // Error Expected
|
||||
Inv string // invocation hint; "http" (default) or "cloudevent"
|
||||
Src string // Source code to check
|
||||
}{
|
||||
{
|
||||
Name: "Instanced HTTP",
|
||||
|
@ -78,10 +50,7 @@ func Handle() { }
|
|||
Name: "Instanced Cloudevent",
|
||||
Sig: InstancedCloudevent,
|
||||
Err: nil,
|
||||
Cfg: func(f Function) Function {
|
||||
f.Invoke = "cloudevent" // see NOTE above
|
||||
return f
|
||||
},
|
||||
Inv: "cloudevent",
|
||||
Src: `
|
||||
package f
|
||||
func New() { }
|
||||
|
@ -90,10 +59,7 @@ func New() { }
|
|||
Name: "Static Cloudevent",
|
||||
Sig: StaticCloudevent,
|
||||
Err: nil,
|
||||
Cfg: func(f Function) Function {
|
||||
f.Invoke = "cloudevent" // see NOTE above
|
||||
return f
|
||||
},
|
||||
Inv: "cloudevent",
|
||||
Src: `
|
||||
package f
|
||||
func Handle() { }
|
||||
|
@ -129,6 +95,19 @@ func New()
|
|||
|
||||
*/
|
||||
func Handle() { }
|
||||
`},
|
||||
{
|
||||
Name: "Instanced with Handler",
|
||||
Sig: InstancedHTTP,
|
||||
Err: nil,
|
||||
Src: `
|
||||
package f
|
||||
|
||||
type F struct{}
|
||||
|
||||
func New() *F { return &F{} }
|
||||
|
||||
func (f *MyFunction) Handle() {}
|
||||
`},
|
||||
}
|
||||
|
||||
|
@ -138,23 +117,11 @@ func Handle() { }
|
|||
root, cleanup := Mktemp(t)
|
||||
defer cleanup()
|
||||
|
||||
f := Function{Runtime: "go", Root: root}
|
||||
if test.Cfg != nil {
|
||||
f = test.Cfg(f)
|
||||
}
|
||||
|
||||
f, err := New().Init(f)
|
||||
if err != nil {
|
||||
if err := os.WriteFile(filepath.Join(root, "function.go"), []byte(test.Src), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// NOTE: if/when the default filename changes from handle.go to
|
||||
// function.go, this will also have to change
|
||||
if err := os.WriteFile(filepath.Join(root, "handle.go"), []byte(test.Src), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s, err := functionSignature(f)
|
||||
s, err := detectSignature(root, "go", test.Inv)
|
||||
if err != nil && test.Err == nil {
|
||||
t.Fatalf("unexpected error. %v", err)
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package scaffolding
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ScaffoldingError struct {
|
||||
Msg string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ScaffoldingError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return fmt.Sprintf("scaffolding error. %v. %v", e.Msg, e.Err)
|
||||
}
|
||||
return fmt.Sprintf("scaffolding error %v", e.Err)
|
||||
}
|
||||
|
||||
func (e ScaffoldingError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
var ErrScaffoldingNotFound = ScaffoldingError{"scaffolding not found", nil}
|
||||
var ErrSignatureNotFound = ScaffoldingError{"supported signature not found", nil}
|
||||
|
||||
type ErrDetectorNotImplemented struct {
|
||||
Runtime string
|
||||
}
|
||||
|
||||
func (e ErrDetectorNotImplemented) Error() string {
|
||||
return fmt.Sprintf("the %v signature detector is not yet available", e.Runtime)
|
||||
}
|
||||
|
||||
type ErrRuntimeNotRecognized struct {
|
||||
Runtime string
|
||||
}
|
||||
|
||||
func (e ErrRuntimeNotRecognized) Error() string {
|
||||
return fmt.Sprintf("signature not found. The runtime %v is not recognized", e.Runtime)
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package scaffolding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"knative.dev/func/pkg/filesystem"
|
||||
)
|
||||
|
||||
// Write scaffolding to a given path
|
||||
//
|
||||
// Scaffolding is a language-level operation which first detects the method
|
||||
// signature used by the function's source code and then writes the
|
||||
// appropriate scaffolding.
|
||||
//
|
||||
// NOTE: Scaffoding is not per-template, because a template is merely an
|
||||
// example starting point for a Function implementation and should have no
|
||||
// bearing on the shape that function can eventually take. The language,
|
||||
// and optionally invocation hint (For cloudevents) are used for this. For
|
||||
// example, there can be multiple templates which exemplify a given method
|
||||
// signature, and the implementation can be switched at any time by the author.
|
||||
// Language, by contrast, is fixed at time of initialization.
|
||||
//
|
||||
// out: the path to output scaffolding
|
||||
// src: the path to the source code to scaffold
|
||||
// runtime: the expected runtime of the target source code "go", "node" etc.
|
||||
// invoke: the optional invocatin hint (default "http")
|
||||
// fs: filesytem which contains scaffolding at '[runtime]/scaffolding'
|
||||
// (exclusive with 'repo')
|
||||
func Write(out, src, runtime, invoke string, fs filesystem.Filesystem) (err error) {
|
||||
|
||||
// detect the signature of the source code in the given location, presuming
|
||||
// a runtime and invocation hint (default "http")
|
||||
s, err := detectSignature(src, runtime, invoke)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Path in the filesystem at which scaffolding is expected to exist
|
||||
d := fmt.Sprintf("%v/scaffolding/%v", runtime, s.String()) // fs uses / on all OSs
|
||||
if _, err := fs.Stat(d); err != nil {
|
||||
return ErrScaffoldingNotFound
|
||||
}
|
||||
|
||||
// Copy from d -> out from the filesystem
|
||||
if err := filesystem.CopyFromFS(d, out, fs); err != nil {
|
||||
return ScaffoldingError{"filesystem copy failed", err}
|
||||
}
|
||||
|
||||
// Replace the 'f' link of the scaffolding (which is now incorrect) to
|
||||
// link to the function's root.
|
||||
rel, err := filepath.Rel(out, src)
|
||||
if err != nil {
|
||||
return ScaffoldingError{"error determining relative path to function source", err}
|
||||
}
|
||||
link := filepath.Join(out, "f")
|
||||
_ = os.Remove(link)
|
||||
if err = os.Symlink(rel, link); err != nil {
|
||||
return fmt.Errorf("error linking scaffolding to source %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// detectSignature returns the Signature of the source code at the given
|
||||
// location assuming a provided runtime and invocation hint.
|
||||
func detectSignature(src, runtime, invoke string) (s Signature, err error) {
|
||||
d, err := newDetector(runtime)
|
||||
if err != nil {
|
||||
return UnknownSignature, err
|
||||
}
|
||||
static, instanced, err := d.Detect(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Function must implement either a static handler or the instanced handler
|
||||
// but not both.
|
||||
if static && instanced {
|
||||
return s, fmt.Errorf("function may not implement both the static and instanced method signatures simultaneously")
|
||||
} else if !static && !instanced {
|
||||
return s, fmt.Errorf("function does not implement any known method signatures")
|
||||
} else if instanced {
|
||||
return toSignature(true, invoke), nil
|
||||
} else {
|
||||
return toSignature(false, invoke), nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
//go:build !integration
|
||||
// +build !integration
|
||||
|
||||
package scaffolding
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"knative.dev/func/pkg/filesystem"
|
||||
|
||||
. "knative.dev/func/pkg/testing"
|
||||
)
|
||||
|
||||
// TestWrite_RuntimeErrors ensures that known runtimes which are not
|
||||
// yet implemented return a "not yet available" message, and unrecognized
|
||||
// runtimes state as much.
|
||||
func TestWrite_RuntimeErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
Runtime string
|
||||
Expected any
|
||||
}{
|
||||
{"go", nil},
|
||||
{"python", nil},
|
||||
{"rust", &ErrDetectorNotImplemented{}},
|
||||
{"node", &ErrDetectorNotImplemented{}},
|
||||
{"typescript", &ErrDetectorNotImplemented{}},
|
||||
{"quarkus", &ErrDetectorNotImplemented{}},
|
||||
{"java", &ErrDetectorNotImplemented{}},
|
||||
{"other", &ErrRuntimeNotRecognized{}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.Runtime, func(t *testing.T) {
|
||||
// Since runtime validation during signature detection is the very first
|
||||
// thing that occurs, we can elide most of the setup and pass zero
|
||||
// values for source directory, output directory and invocation.
|
||||
// This may need to be expanded in the event the Write function is
|
||||
// expanded to have more preconditions.
|
||||
err := Write("", "", test.Runtime, "", nil)
|
||||
if test.Expected != nil && err == nil {
|
||||
t.Fatalf("expected runtime %v to yield a detection error", test.Runtime)
|
||||
}
|
||||
if test.Expected != nil && !errors.As(err, test.Expected) {
|
||||
t.Fatalf("did not receive expected error type for %v runtime.", test.Runtime)
|
||||
}
|
||||
t.Logf("ok: %v", err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrite ensures that the Write method writes Scaffolding to the given
|
||||
// destination. This is a failry shallow test. See the Scaffolding and
|
||||
// Detector tests for more depth.
|
||||
func TestWrite(t *testing.T) {
|
||||
// The filesystem containing scaffolding is expected to conform to the
|
||||
// structure:
|
||||
// /[language]/scaffolding/["instanced"|"static"]-[invocation]
|
||||
// ex:
|
||||
// "./go/scaffolding/instanced-http/main.go"
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fs := filesystem.NewOsFilesystem(filepath.Join(cwd, "testdata", "testwrite"))
|
||||
|
||||
root, done := Mktemp(t)
|
||||
defer done()
|
||||
|
||||
// Write out a test implementation that will result in the InstancedHTTP
|
||||
// signature being detected.
|
||||
impl := `
|
||||
package f
|
||||
|
||||
type F struct{}
|
||||
|
||||
func New() *F { return nil }
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(root, "f.go"), []byte(impl), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The output destination for the scaffolding
|
||||
out := filepath.Join(root, "out")
|
||||
|
||||
// Write Scaffolding to
|
||||
err = Write(
|
||||
out, // output directory
|
||||
root, // source code location
|
||||
"go", // Runtime
|
||||
"", // optional invocation hint (http is the default)
|
||||
fs) // The filesystem from which the scaffolding should be pulled
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Assert there exists a main.go (from the testdata scaffolding filesystem).
|
||||
if _, err = os.Stat(filepath.Join(root, "out", "main.go")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Assert there exists a symbolic link to the source code
|
||||
root, err = filepath.EvalSymlinks(root) // dereference any current symlinks
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target, err := filepath.EvalSymlinks(filepath.Join(out, "f"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if target != root {
|
||||
t.Fatalf("scaffolding symlink should be:\n%v\n But got target:\n%v", root, target)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestWrite_ScaffoldingNotFound ensures that a typed error is returned
|
||||
// when scaffolding is not found.
|
||||
func TestWrite_ScaffoldingNotFound(t *testing.T) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fs := filesystem.NewOsFilesystem(filepath.Join(cwd, "testdata", "testnotfound"))
|
||||
|
||||
root, done := Mktemp(t)
|
||||
defer done()
|
||||
|
||||
impl := `
|
||||
package f
|
||||
|
||||
type F struct{}
|
||||
|
||||
func New() *F { return nil }
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(root, "f.go"), []byte(impl), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out := filepath.Join(root, "out")
|
||||
|
||||
err = Write(out, root, "go", "", fs)
|
||||
if err == nil {
|
||||
t.Fatal("did not receive expected error")
|
||||
}
|
||||
if !errors.Is(err, ErrScaffoldingNotFound) {
|
||||
t.Fatalf("error received was not ErrScaffoldingNotFound. %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewScaffoldingError ensures that a scaffolding error wraps its
|
||||
// underlying error such that callers can use errors.Is/As.
|
||||
func TestNewScaffoldingError(t *testing.T) {
|
||||
|
||||
// exampleError that would come from something scaffolding employs to
|
||||
// accomplish a task
|
||||
var ExampleError = errors.New("example error")
|
||||
|
||||
err := ScaffoldingError{"some ExampleError", ExampleError}
|
||||
|
||||
if !errors.Is(err, ExampleError) {
|
||||
t.Fatalf("type ScaffoldingError does not wrap errors.")
|
||||
}
|
||||
t.Logf("ok: %v", err)
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package scaffolding
|
||||
|
||||
type Signature int
|
||||
|
||||
const (
|
||||
UnknownSignature Signature = iota
|
||||
InstancedHTTP
|
||||
InstancedCloudevent
|
||||
StaticHTTP
|
||||
StaticCloudevent
|
||||
)
|
||||
|
||||
func (s Signature) String() string {
|
||||
return []string{
|
||||
"unknown",
|
||||
"instanced-http",
|
||||
"instanced-cloudevent",
|
||||
"static-http",
|
||||
"static-cloudevent",
|
||||
}[s]
|
||||
}
|
||||
|
||||
var signatureMap = map[bool]map[string]Signature{
|
||||
true: {
|
||||
"http": InstancedHTTP,
|
||||
"cloudevent": InstancedCloudevent},
|
||||
false: {
|
||||
"http": StaticHTTP,
|
||||
"cloudevent": StaticCloudevent},
|
||||
}
|
||||
|
||||
// toSignature converts an instanced boolean and invocation hint into
|
||||
// a Signature enum.
|
||||
func toSignature(instanced bool, invoke string) Signature {
|
||||
if invoke == "" {
|
||||
invoke = "http"
|
||||
}
|
||||
s, ok := signatureMap[instanced][invoke]
|
||||
if !ok {
|
||||
return UnknownSignature
|
||||
}
|
||||
return s
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package scaffolding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSignatures ensures that the enum of signatures is properly formed.
|
||||
// - The correct signature enum is returned from ToSignature
|
||||
// - The string method is correctly indexed
|
||||
// - "http" is the default for invocation hint
|
||||
func TestSignatures(t *testing.T) {
|
||||
|
||||
// This is a brute-force implementation which simply runs the logic in
|
||||
// reverse. Basically just checking ones work when the enum is modified.
|
||||
|
||||
tests := []struct {
|
||||
instanced bool // signatures are brodly classified into instanced or static
|
||||
invocation string // the invocation hint (default is "http")
|
||||
expectedEnum Signature // the expected enum
|
||||
expectedName string // the expected string
|
||||
}{
|
||||
{true, "", InstancedHTTP, "instanced-http"},
|
||||
{true, "http", InstancedHTTP, "instanced-http"},
|
||||
{true, "cloudevent", InstancedCloudevent, "instanced-cloudevent"},
|
||||
{false, "", StaticHTTP, "static-http"},
|
||||
{false, "http", StaticHTTP, "static-http"},
|
||||
{false, "cloudevent", StaticCloudevent, "static-cloudevent"},
|
||||
{true, "invalid", UnknownSignature, "unknown"},
|
||||
{false, "invalid", UnknownSignature, "unknown"},
|
||||
}
|
||||
|
||||
testName := func(instanced bool, invocation string) string {
|
||||
instancedString := "instanced"
|
||||
if !instanced {
|
||||
instancedString = "static"
|
||||
}
|
||||
invocationString := "default"
|
||||
if invocation != "" {
|
||||
invocationString = invocation
|
||||
}
|
||||
return fmt.Sprintf("%v-%v", instancedString, invocationString)
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(testName(test.instanced, test.invocation), func(t *testing.T) {
|
||||
|
||||
signature := toSignature(test.instanced, test.invocation)
|
||||
|
||||
if signature != test.expectedEnum {
|
||||
t.Fatal("enum incorrectly mapped.")
|
||||
}
|
||||
if signature.String() != test.expectedName {
|
||||
t.Fatal("string representation incorrectly mapped")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# TestNotFound
|
||||
|
||||
An example of a template runtime directory which lacks scaffolding.
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hello, World!")
|
||||
}
|
Loading…
Reference in New Issue