At a high level, these areas make up the github.com/listendev/lstn
project:
cmd/
- implementation for individuallstn
commandspkg/
- most other packages and librariesdocs/
- documentation for maintainers and contributorsmake/
- build and release scriptsinternal/
- packages highly specific to our needs and thus internalgo.mod
- external Go dependencies for this project, automatically fetched at build time
Running lstn help in
displays the help for a child command or for a topic.
The naming convention for commands is:
cmd/<command>/<command.go>
In case of nested commands:
cmd/<command>/<subcommand>/<subcommand.go>
Following the above example, the main implementation for the lstn in
command (including its help) is in cmd/in/in.go.
Other help topics not specific to any command, for example lsnt help environment
, are found in pkg/cmd/help/topics.go.
TODO: During our release process, these help topics are automatically converted to manual pages.
To illustrate how the lstn
CLI works in its typical mode of operation, let's build the project, run a command,
and talk through which code gets run.
go build -o make/make make/main.go
- Compiles the binary to buildlstn
make/make lstn
- Makes sure all external Go dependencies are fetched, then compiles thelstn
binary./lstn in --json ./ciao
- Runs the newly buildlstn
binary and passes the following flags and arguments to the process:["in", "--json", "./ciao"]
- The
main
package sets up the CLI flags, its subcommands, its context, and dispatches the execution to the "root" command with therootCmd.ExecuteContext()
method - The root command represents the top-level
lstn
command and knows how to dispatch execution to any other gh command nested under it - Because of the
in
argument, the execution reaches theRunE()
function of thecobra.Command
within cmd/in/in.go - Because of the
--json
flag, theinOpts.Json
value is set totrue
- The logic of the
RunE()
function of thein
subcommand looks for apackage.json
inside the./ciao
target directory - The
in
logic looks for thenpm
binary, creates apackage-lock.json
on the fly for thepackage.json
in the./ciao
target directory (if any) - Then, the logic of the
RunE()
function of thein
subcommand queries thenpm
registry to collect the sha sums of all the dependencies of thepackage-lock.json
- Finally, the
in
logic queries the listen.dev API asking for the analysis verdicts of all the dependencies - The response (if any) gets print in JSON form
- The program execution is now back at
func Boot()
in root.go - In case there were any Go error as a result of processing the command, the function will abort the process with a non-zero exit status
- Otherwise, the process ends with status 0 meaning success.
- First, check on our issue tracker to verify that our team had approved the plans for a new command.
- Create a package for the new command, e.g. for a new command
lstn snitch
create the following directory structure:cmd/snitch/
- The new package should expose a
New()
function that accepts a*context.Context
type and returns(*cobra.Command, error)
. - Use the method from the previous step to generate the command and add it to the command tree
- Typically this means adding it as subcommand of the root command (ie.,
cmd/root/root.go
) in itsNew()
function.
- Typically this means adding it as subcommand of the root command (ie.,
This task might be tricky for some edge things we do in lstn
.
For example, lstn
looks for the npm
(and - in the future - for other package managers) binary. Then it uses npm
to generate a package-lock.json
file in a temporary directory.
Naturally and generally, none of these things should ever happen for real when running tests, unless you are sure that any filesystem operations are strictly scoped to a location made for and maintained by the test itself.
Where possible we change the active FS during testing. You can take a look at how we do it in pkg/git/config_test.go.
Specifically to those bits:
func TestGitConfig(t *testing.T) {
fs := memfs.New()
activeFS = fs
defer func() { activeFS = defaultFS() }()
// ...
}
In other cases, ie. when we look for the npm
binary using the Go exec
package, we let the test binary itself pretend to be npm
and we let the specific test call such a binary.
You can take a look at this approach by inspecting pkg/npm/main_test.go and pkg/npm/packagelock_test.go.
Finally, lstn
also performs HTTP calls to our servers.
To avoid actually running things like making real API requests we stub the listen.dev server with the httptest
Go package (see examples in pkg/listen/listen_test.go).
These bits of functionality to help us test lstn
are placed into the internal/testing directory.
As a general rule of thumb, to make your code testable write small and isolated pieces of functionality that are designed to be composed together.
Prefer table-driven tests for maintaining variations of different test inputs and expectations when exercising a single piece of functionality.