[engine] Fix crash in Doc()
While looking at vendoring support, I realized that a load() statement
without a shac.textproto would cause a crash.
Improve error messages and test coverage by a whopping 0.4%.
Change-Id: Iba84274452a75d1d760d99841d4646b191d6fadd
Reviewed-on: https://fuchsia-review.googlesource.com/c/shac-project/shac/+/913034
Reviewed-by: Oliver Newman <olivernewman@google.com>
Commit-Queue: Auto-Submit <auto-submit@fuchsia-infra.iam.gserviceaccount.com>
Fuchsia-Auto-Submit: Marc-Antoine Ruel <maruel@google.com>
diff --git a/internal/engine/docgen.go b/internal/engine/docgen.go
index 22bb510..2cb33a4 100644
--- a/internal/engine/docgen.go
+++ b/internal/engine/docgen.go
@@ -59,6 +59,9 @@
}
b, err := os.ReadFile(src)
if err != nil {
+ if errors.Is(err, fs.ErrNotExist) {
+ return "", fmt.Errorf("file %s not found", src)
+ }
return "", err
}
content = string(b)
@@ -90,6 +93,7 @@
}
}
// Load packages to get the exported symbols.
+ // TODO(maruel): Persist cache.
pkgMgr := PackageManager{Root: tmpdir}
var packages map[string]fs.FS
root := filepath.Dir(src)
@@ -100,12 +104,17 @@
if err = prototext.Unmarshal(b, &doc); err != nil {
return "", err
}
- // TODO(maruel): Only fetch the direct ones!
+ if err = doc.Validate(); err != nil {
+ return "", err
+ }
if packages, err = pkgMgr.RetrievePackages(context.Background(), root, &doc); err != nil {
return "", err
}
} else if !errors.Is(err, fs.ErrNotExist) {
return "", err
+ } else {
+ // Still allow local access even if no shac.textproto is present.
+ packages = map[string]fs.FS{"__main__": os.DirFS(root)}
}
}
d := m.Doc()
@@ -184,7 +193,14 @@
} else if strings.HasPrefix(m, "//") {
relpath = m[2:]
}
- d, err := packages[pkg].Open(relpath)
+ ref := packages[pkg]
+ if ref == nil {
+ return "", fmt.Errorf("package %s not found", pkg)
+ }
+ d, err := ref.Open(relpath)
+ if errors.Is(err, fs.ErrNotExist) {
+ return "", errors.New("file not found")
+ }
if err != nil {
return "", err
}
diff --git a/internal/engine/docgen_test.go b/internal/engine/docgen_test.go
index e412ff0..c349a77 100644
--- a/internal/engine/docgen_test.go
+++ b/internal/engine/docgen_test.go
@@ -17,6 +17,7 @@
import (
"os"
"path/filepath"
+ "runtime"
"strconv"
"strings"
"testing"
@@ -56,6 +57,50 @@
}
}
+func TestDoc_Error(t *testing.T) {
+ t.Parallel()
+ data := []struct {
+ path string
+ err string
+ }{
+ {
+ "file",
+ "invalid source file name, expecting .star suffix",
+ },
+ {
+ "@remote//file.star",
+ "todo: implement @module",
+ },
+ {
+ "/non_existent.star",
+ func() string {
+ if runtime.GOOS == "windows" {
+ p, err := filepath.Abs("/non_existent.star")
+ if err != nil {
+ t.Fatal(err)
+ }
+ return "file " + p + " not found"
+ }
+ return "file /non_existent.star not found"
+ }(),
+ },
+ }
+ for i, line := range data {
+ line := line
+ t.Run(strconv.Itoa(i), func(t *testing.T) {
+ t.Parallel()
+ d, err := Doc(line.path)
+ if err == nil {
+ t.Fatalf("expected error, got: %s", d)
+ }
+ got := err.Error()
+ if diff := cmp.Diff(line.err, got); diff != "" {
+ t.Fatalf("mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
+
func TestDocTemplate(t *testing.T) {
t.Parallel()
data := []struct {
@@ -88,6 +133,7 @@
line := line
t.Run(strconv.Itoa(i), func(t *testing.T) {
t.Parallel()
+ // This test case cannot call load() because "main.star" is not an absolute path.
got, err := genDoc(t.TempDir(), "main.star", line.in, false)
if err != nil {
t.Fatal(err)
@@ -99,6 +145,66 @@
}
}
+func TestDocTemplate_Testdata_Err(t *testing.T) {
+ t.Parallel()
+ data := []struct {
+ path string
+ err string
+ }{
+ {
+ "testdata/docgen/err/missing_local/test.star",
+ `template: main:115:13: executing "main" at <Symbol "testdata/docgen/err/missing_local/test.star" "fn">: error calling Symbol: in testdata/docgen/err/missing_local/test.star: in //non_existent.star: file not found`,
+ },
+ {
+ "testdata/docgen/err/missing_pkg/test.star",
+ `template: main:115:13: executing "main" at <Symbol "testdata/docgen/err/missing_pkg/test.star" "fn">: error calling Symbol: in testdata/docgen/err/missing_pkg/test.star: in @remote//non_existent.star: package remote not found`,
+ },
+ {
+ "testdata/docgen/err/textproto_is_dir/test.star",
+ func() string {
+ if runtime.GOOS == "windows" {
+ return "read testdata\\docgen\\err\\textproto_is_dir\\shac.textproto: Incorrect function."
+ }
+ return "read testdata/docgen/err/textproto_is_dir/shac.textproto: is a directory"
+ }(),
+ },
+ }
+ for i, line := range data {
+ line := line
+ t.Run(strconv.Itoa(i), func(t *testing.T) {
+ in, err := os.ReadFile(line.path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ d, err := genDoc(filepath.Dir(line.path), line.path, string(in), false)
+ if err == nil {
+ t.Fatalf("expected error, got: %s", d)
+ }
+ got := err.Error()
+ if diff := cmp.Diff(line.err, got); diff != "" {
+ t.Fatalf("mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
+
+func TestDocTemplateLoad_Load_Local(t *testing.T) {
+ t.Parallel()
+ f := "testdata/docgen/load_local/test.star"
+ in, err := os.ReadFile(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := genDoc(filepath.Dir(f), f, string(in), false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := "# testdata/docgen/load_local/test.star\n\n## Table of contents\n\n- [fn](#fn)\n- [sym](#sym)\n\n## fn\n\nDo stuff.\n\n\n## sym\n\n\n\nFields:\n\n- fn\n\n## sym.fn\n\nDo stuff.\n\n"
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Fatalf("mismatch (-want +got):\n%s", diff)
+ }
+}
+
func TestDocgenGenerator(t *testing.T) {
// It's not really a unit test, it's more to document parts of what is
// available in the template engine.
diff --git a/internal/engine/testdata/docgen/err/missing_local/test.star b/internal/engine/testdata/docgen/err/missing_local/test.star
new file mode 100644
index 0000000..7e1b7d2
--- /dev/null
+++ b/internal/engine/testdata/docgen/err/missing_local/test.star
@@ -0,0 +1,15 @@
+# Copyright 2023 The Shac Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load("//non_existent.star", fn = "fn")
diff --git a/internal/engine/testdata/docgen/err/missing_pkg/test.star b/internal/engine/testdata/docgen/err/missing_pkg/test.star
new file mode 100644
index 0000000..9af78ec
--- /dev/null
+++ b/internal/engine/testdata/docgen/err/missing_pkg/test.star
@@ -0,0 +1,15 @@
+# Copyright 2023 The Shac Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load("@remote//non_existent.star", fn = "fn")
diff --git a/internal/engine/testdata/docgen/err/textproto_is_dir/shac.textproto/.gitignore b/internal/engine/testdata/docgen/err/textproto_is_dir/shac.textproto/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/internal/engine/testdata/docgen/err/textproto_is_dir/shac.textproto/.gitignore
diff --git a/internal/engine/testdata/docgen/err/textproto_is_dir/test.star b/internal/engine/testdata/docgen/err/textproto_is_dir/test.star
new file mode 100644
index 0000000..d5a9020
--- /dev/null
+++ b/internal/engine/testdata/docgen/err/textproto_is_dir/test.star
@@ -0,0 +1,15 @@
+# Copyright 2023 The Shac Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+pass
diff --git a/internal/engine/testdata/docgen/load_local/common.star b/internal/engine/testdata/docgen/load_local/common.star
new file mode 100644
index 0000000..1c34e4c
--- /dev/null
+++ b/internal/engine/testdata/docgen/load_local/common.star
@@ -0,0 +1,17 @@
+# Copyright 2023 The Shac Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def fn():
+ """Do stuff."""
+ pass
diff --git a/internal/engine/testdata/docgen/load_local/test.star b/internal/engine/testdata/docgen/load_local/test.star
new file mode 100644
index 0000000..cb3cea9
--- /dev/null
+++ b/internal/engine/testdata/docgen/load_local/test.star
@@ -0,0 +1,19 @@
+# Copyright 2023 The Shac Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load("//common.star", fn = "fn")
+
+sym = struct(
+ fn = fn,
+)