Add use unit tests and tweak documentation

This explicitly tests a common error case not currently verified by the
existing unit tests. Thus improving test coverage by one line.  :-)
This commit is contained in:
Kurtis Rader 2021-09-27 20:55:12 -07:00 committed by Qi Xiao
parent 7350a3351b
commit 8faf8930b3
3 changed files with 56 additions and 24 deletions

View File

@ -36,9 +36,11 @@ var builtinSpecials map[string]compileBuiltin
// intended for external consumption, e.g. the syntax highlighter. // intended for external consumption, e.g. the syntax highlighter.
var IsBuiltinSpecial = map[string]bool{} var IsBuiltinSpecial = map[string]bool{}
type noSuchModule struct{ spec string } // NoSuchModule encodes an error where a module spec cannot be resolved.
type NoSuchModule struct{ spec string }
func (err noSuchModule) Error() string { return "no such module: " + err.spec } // Error implements the error interface.
func (err NoSuchModule) Error() string { return "no such module: " + err.spec }
func init() { func init() {
// Needed to avoid initialization loop // Needed to avoid initialization loop
@ -316,7 +318,12 @@ func (op useOp) exec(fm *Frame) Exception {
return nil return nil
} }
// TODO: Add support for module specs relative to a package/workspace.
// See https://github.com/elves/elvish/issues/1421.
func use(fm *Frame, spec string, r diag.Ranger) (*Ns, error) { func use(fm *Frame, spec string, r diag.Ranger) (*Ns, error) {
// Handle relative imports. Note that this deliberately does not support Windows backslash as a
// path separator because module specs are meant to be platform independent. If necessary, we
// translate a module spec to an appropriate path for the platform.
if strings.HasPrefix(spec, "./") || strings.HasPrefix(spec, "../") { if strings.HasPrefix(spec, "./") || strings.HasPrefix(spec, "../") {
var dir string var dir string
if fm.srcMeta.IsFile { if fm.srcMeta.IsFile {
@ -331,6 +338,8 @@ func use(fm *Frame, spec string, r diag.Ranger) (*Ns, error) {
path := filepath.Clean(dir + "/" + spec) path := filepath.Clean(dir + "/" + spec)
return useFromFile(fm, spec, path, r) return useFromFile(fm, spec, path, r)
} }
// Handle imports of pre-defined modules like `builtin` and `str`.
if ns, ok := fm.Evaler.modules[spec]; ok { if ns, ok := fm.Evaler.modules[spec]; ok {
return ns, nil return ns, nil
} }
@ -338,16 +347,21 @@ func use(fm *Frame, spec string, r diag.Ranger) (*Ns, error) {
return evalModule(fm, spec, return evalModule(fm, spec,
parse.Source{Name: "[bundled " + spec + "]", Code: code}, r) parse.Source{Name: "[bundled " + spec + "]", Code: code}, r)
} }
// Handle imports relative to the Elvish module search directories.
//
// TODO: For non-relative imports, use the spec (instead of the full path) // TODO: For non-relative imports, use the spec (instead of the full path)
// as the module key instead to avoid searching every time. // as the module key instead to avoid searching every time.
for _, dir := range fm.Evaler.LibDirs { for _, dir := range fm.Evaler.LibDirs {
ns, err := useFromFile(fm, spec, filepath.Join(dir, spec), r) ns, err := useFromFile(fm, spec, filepath.Join(dir, spec), r)
if _, isNoSuchModule := err.(noSuchModule); isNoSuchModule { if _, isNoSuchModule := err.(NoSuchModule); isNoSuchModule {
continue continue
} }
return ns, err return ns, err
} }
return nil, noSuchModule{spec}
// Sadly, we couldn't resolve the module spec.
return nil, NoSuchModule{spec}
} }
// TODO: Make access to fm.Evaler.modules concurrency-safe. // TODO: Make access to fm.Evaler.modules concurrency-safe.
@ -360,7 +374,7 @@ func useFromFile(fm *Frame, spec, path string, r diag.Ranger) (*Ns, error) {
code, err := readFileUTF8(path + ".elv") code, err := readFileUTF8(path + ".elv")
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil, noSuchModule{spec} return nil, NoSuchModule{spec}
} }
return nil, err return nil, err
} }
@ -370,7 +384,7 @@ func useFromFile(fm *Frame, spec, path string, r diag.Ranger) (*Ns, error) {
plug, err := pluginOpen(path + ".so") plug, err := pluginOpen(path + ".so")
if err != nil { if err != nil {
return nil, noSuchModule{spec} return nil, NoSuchModule{spec}
} }
sym, err := plug.Lookup("Ns") sym, err := plug.Lookup("Ns")
if err != nil { if err != nil {
@ -378,7 +392,7 @@ func useFromFile(fm *Frame, spec, path string, r diag.Ranger) (*Ns, error) {
} }
ns, ok := sym.(**Ns) ns, ok := sym.(**Ns)
if !ok { if !ok {
return nil, noSuchModule{spec} return nil, NoSuchModule{spec}
} }
fm.Evaler.modules[path] = *ns fm.Evaler.modules[path] = *ns
return *ns, nil return *ns, nil

View File

@ -349,13 +349,11 @@ func TestUse_SupportsCircularDependency(t *testing.T) {
func TestUse(t *testing.T) { func TestUse(t *testing.T) {
libdir1 := InTempDir(t) libdir1 := InTempDir(t)
ApplyDir(Dir{ ApplyDir(Dir{
"shadow.elv": "put lib1", "shadow.elv": "put lib1",
}) })
libdir2 := InTempDir(t) libdir2 := InTempDir(t)
ApplyDir(Dir{ ApplyDir(Dir{
"has-init.elv": "put has-init", "has-init.elv": "put has-init",
"put-x.elv": "put $x", "put-x.elv": "put $x",
@ -410,7 +408,10 @@ func TestUse(t *testing.T) {
// modules // modules
That("x = foo; use put-x").Throws(AnyError), That("x = foo; use put-x").Throws(AnyError),
// TODO: Test module namespace // Using an unknown module spec fails.
That("use unknown").Throws(ErrorWithType(NoSuchModule{})),
That("use ./unknown").Throws(ErrorWithType(NoSuchModule{})),
That("use ../unknown").Throws(ErrorWithType(NoSuchModule{})),
// Nonexistent module // Nonexistent module
That("use non-existent").Throws(ErrorWithMessage("no such module: non-existent")), That("use non-existent").Throws(ErrorWithMessage("no such module: non-existent")),

View File

@ -169,10 +169,11 @@ writes out `\*`.
A string is a (possibly empty) sequence of bytes. A string is a (possibly empty) sequence of bytes.
[Single-quoted string literals](#single-quoted-string), [Single-quoted string literals](#single-quoted-string),
[double-quoted string literals](#double-quoted-string)and [barewords](#bareword) [double-quoted string literals](#double-quoted-string) and
all evaluate to string values. Unless otherwise noted, different syntaxes of [barewords](#bareword) all evaluate to string values. Unless otherwise noted,
string literals are equivalent in the code. For instance, `xyz`, `'xyz'` and different syntaxes of string literals are equivalent in the code. For instance,
`"xyz"` are different syntaxes for the same string with content `xyz`. `xyz`, `'xyz'` and `"xyz"` are different syntaxes for the same string with
content `xyz`.
Strings that contain UTF-8 encoded text can be [indexed](#indexing) with a Strings that contain UTF-8 encoded text can be [indexed](#indexing) with a
**byte index** where a codepoint starts, which results in the codepoint that **byte index** where a codepoint starts, which results in the codepoint that
@ -2392,18 +2393,34 @@ itself or defined by the user.
### Importing modules with `use` ### Importing modules with `use`
Modules are imported using the `use` special command, which accepts a **module Modules are imported using the `use` special command. It requires a **module
spec** and an optional alias: spec** and allows a namespace alias:
```elvish ```elvish
use $spec $alias? use $spec $alias?
``` ```
Both the module spec and the alias must appear as a single string literal. The module spec and the alias must both be a simple [string literal](#string).
[Compound strings](#compounding) such as `'a'/b` are not allowed.
The module spec specifies which module to import, and the alias, if given, The module spec specifies which module to import. The alias, if given, specifies
specifies the namespace to import the module under. By default, the namespace is the namespace to import the module under. By default, the namespace is derived
derived from the module spec, by taking the part after the last slash. from the module spec by taking the part after the last slash.
Module specs fall into three categories that are resolved in the following
order:
1. **Relative**: These are [relative](#relative-imports) to the file containing
the `use` command.
1. **User defined**: These match a [user defined module](#user-defined-modules)
in a [module search directory](#module-search-directories).
1. **Pre-defined**: These match the name of a
[pre-defined module](#pre-defined-modules), such as `math` or `str`.
If a module spec doesn't match any of the above a "no such module"
[exception](#exception) is raised.
Examples: Examples:
@ -2525,10 +2542,10 @@ functions.
### Relative imports ### Relative imports
The module spec may being with `./` or `../`, which introduce **relative The module spec may begin with `./` or `../` to introduce a **relative import**.
imports**. When `use` is invoked from a file, this will import the file relative When `use` is invoked from a file this will import the file relative to the
to the location of the file. When `use` is invoked at the interactive prompt, location of the file. When `use` is invoked from an interactive prompt, this
this will import the file relative to the current working directory. will import the file relative to the current working directory.
### Scoping of imports ### Scoping of imports