Skip to content

Commit b5f21c1

Browse files
Implemented mocking mechanism
This commit implements the choice of back-end, and the back-end interface. A mock back-end that always errors out is implemented so as to fit the interface.
1 parent f60bcba commit b5f21c1

24 files changed

+511
-212
lines changed

backend.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//go:build !mock
2+
3+
package gowsl
4+
5+
import (
6+
"context"
7+
8+
"github.com/ubuntu/gowsl/internal/interfaces"
9+
"github.com/ubuntu/gowsl/internal/windows"
10+
)
11+
12+
func selectBackend(ctx context.Context) interfaces.Backend {
13+
return windows.Backend{}
14+
}

backend_mock.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//go:build mock
2+
3+
package gowsl
4+
5+
import (
6+
"context"
7+
8+
"github.com/ubuntu/gowsl/internal/interfaces"
9+
"github.com/ubuntu/gowsl/internal/mock"
10+
"github.com/ubuntu/gowsl/internal/windows"
11+
)
12+
13+
type backendQueryType int
14+
15+
const backendQuery backendQueryType = 0
16+
17+
// WithMock adds the mock back-end to the context.
18+
func WithMock(ctx context.Context) context.Context {
19+
return context.WithValue(ctx, backendQuery, mock.Backend{})
20+
}
21+
22+
func selectBackend(ctx context.Context) interfaces.Backend {
23+
v := ctx.Value(backendQuery)
24+
25+
if v == nil {
26+
return windows.Backend{}
27+
}
28+
29+
//nolint: forcetypeassert // The panic is expected and welcome
30+
return v.(interfaces.Backend)
31+
}

distro.go

Lines changed: 39 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,29 @@ package gowsl
77
// This file contains utilities to interact with a Distro and its configuration
88

99
import (
10+
"context"
1011
"errors"
1112
"fmt"
1213
"sort"
1314

1415
"github.com/google/uuid"
1516
"github.com/ubuntu/decorate"
17+
"github.com/ubuntu/gowsl/internal/flags"
1618
)
1719

1820
// Distro is an abstraction around a WSL distro.
1921
type Distro struct {
22+
ctx context.Context
2023
name string
2124
}
2225

2326
// NewDistro declares a new distribution, but does not register it nor
2427
// check if it exists.
25-
func NewDistro(name string) Distro {
26-
return Distro{name: name}
28+
func NewDistro(ctx context.Context, name string) Distro {
29+
return Distro{
30+
ctx: ctx,
31+
name: name,
32+
}
2733
}
2834

2935
// Name is a getter for the DistroName as shown in "wsl.exe --list".
@@ -35,7 +41,7 @@ func (d Distro) Name() string {
3541
func (d *Distro) GUID() (id uuid.UUID, err error) {
3642
defer decorate.OnError(&err, "could not obtain GUID of %s", d.name)
3743

38-
distros, err := registeredDistros()
44+
distros, err := registeredDistros(d.ctx)
3945
if err != nil {
4046
return id, err
4147
}
@@ -50,38 +56,38 @@ func (d *Distro) GUID() (id uuid.UUID, err error) {
5056
// Equivalent to:
5157
//
5258
// wsl --terminate <distro>
53-
func (d Distro) Terminate() error {
54-
return terminate(d.Name())
59+
func (d Distro) Terminate(ctx context.Context) error {
60+
return selectBackend(ctx).Terminate(d.Name())
5561
}
5662

5763
// Shutdown powers off all of WSL, including all other distros.
5864
// Equivalent to:
5965
//
6066
// wsl --shutdown
61-
func Shutdown() error {
62-
return shutdown()
67+
func Shutdown(ctx context.Context) error {
68+
return selectBackend(ctx).Shutdown()
6369
}
6470

6571
// SetAsDefault sets a particular distribution as the default one.
6672
// Equivalent to:
6773
//
6874
// wsl --set-default <distro>
69-
func (d Distro) SetAsDefault() error {
70-
return setAsDefault(d.Name())
75+
func (d Distro) SetAsDefault(ctx context.Context) error {
76+
return selectBackend(ctx).SetAsDefault(d.Name())
7177
}
7278

7379
// DefaultDistro gets the current default distribution.
74-
func DefaultDistro() (d Distro, err error) {
80+
func DefaultDistro(ctx context.Context) (d Distro, err error) {
7581
defer decorate.OnError(&err, "could not obtain the default distro")
7682

7783
// First, we find out the GUID of the default distro
78-
r, err := openRegistry(lxssPath)
84+
r, err := selectBackend(ctx).OpenLxssRegistry()
7985
if err != nil {
8086
return d, err
8187
}
82-
defer r.close()
88+
defer r.Close()
8389

84-
guid, err := r.field("DefaultDistribution")
90+
guid, err := r.Field("DefaultDistribution")
8591
if err != nil {
8692
return d, err
8793
}
@@ -92,37 +98,20 @@ func DefaultDistro() (d Distro, err error) {
9298
}
9399

94100
// Last, we find out the name of the distro
95-
r, err = openRegistry(lxssPath, guid)
101+
r, err = selectBackend(ctx).OpenLxssRegistry(guid)
96102
if err != nil {
97103
return d, err
98104
}
99-
defer r.close()
105+
defer r.Close()
100106

101-
name, err := r.field("DistributionName")
107+
name, err := r.Field("DistributionName")
102108
if err != nil {
103109
return d, err
104110
}
105111

106-
return NewDistro(name), err
112+
return NewDistro(ctx, name), err
107113
}
108114

109-
// Windows' WSL_DISTRIBUTION_FLAGS
110-
// https://learn.microsoft.com/en-us/windows/win32/api/wslapi/ne-wslapi-wsl_distribution_flags
111-
type wslFlags int
112-
113-
// Allowing underscores in names to keep it as close to Windows as possible.
114-
const (
115-
flag_NONE wslFlags = 0x0 //nolint: revive
116-
flag_ENABLE_INTEROP wslFlags = 0x1 //nolint: revive
117-
flag_APPEND_NT_PATH wslFlags = 0x2 //nolint: revive
118-
flag_ENABLE_DRIVE_MOUNTING wslFlags = 0x4 //nolint: revive
119-
120-
// Per the conversation at https://github.com/microsoft/WSL-DistroLauncher/issues/96
121-
// the information about version 1 or 2 is on the 4th bit of the flags, which is
122-
// currently referenced neither by the API nor the documentation.
123-
flag_undocumented_WSL_VERSION wslFlags = 0x8 //nolint: revive
124-
)
125-
126115
// Configuration is the configuration of the distro.
127116
type Configuration struct {
128117
Version uint8 // Type of filesystem used (lxfs vs. wslfs, relevant only to WSL1)
@@ -192,9 +181,9 @@ func (d Distro) GetConfiguration() (c Configuration, err error) {
192181
defer decorate.OnError(&err, "could not access configuration for %s", d.name)
193182

194183
var conf Configuration
195-
var flags wslFlags
184+
var flags flags.WslFlags
196185

197-
err = wslGetDistributionConfiguration(
186+
err = selectBackend(d.ctx).WslGetDistributionConfiguration(
198187
d.Name(),
199188
&conf.Version,
200189
&conf.DefaultUID,
@@ -280,55 +269,55 @@ func (d *Distro) configure(config Configuration) error {
280269
return err
281270
}
282271

283-
return wslConfigureDistribution(d.Name(), config.DefaultUID, flags)
272+
return selectBackend(d.ctx).WslConfigureDistribution(d.Name(), config.DefaultUID, flags)
284273
}
285274

286275
// unpackFlags examines a winWslFlags object and stores its findings in the Configuration.
287-
func (conf *Configuration) unpackFlags(flags wslFlags) {
276+
func (conf *Configuration) unpackFlags(f flags.WslFlags) {
288277
conf.InteropEnabled = false
289-
if flags&flag_ENABLE_INTEROP != 0 {
278+
if flags.ENABLE_INTEROP != 0 {
290279
conf.InteropEnabled = true
291280
}
292281

293282
conf.PathAppended = false
294-
if flags&flag_APPEND_NT_PATH != 0 {
283+
if f&flags.APPEND_NT_PATH != 0 {
295284
conf.PathAppended = true
296285
}
297286

298287
conf.DriveMountingEnabled = false
299-
if flags&flag_ENABLE_DRIVE_MOUNTING != 0 {
288+
if f&flags.ENABLE_DRIVE_MOUNTING != 0 {
300289
conf.DriveMountingEnabled = true
301290
}
302291

303292
conf.undocumentedWSLVersion = 1
304-
if flags&flag_undocumented_WSL_VERSION != 0 {
293+
if f&flags.Undocumented_WSL_VERSION != 0 {
305294
conf.undocumentedWSLVersion = 2
306295
}
307296
}
308297

309298
// packFlags generates a winWslFlags object from the Configuration.
310-
func (conf Configuration) packFlags() (wslFlags, error) {
311-
flags := flag_NONE
299+
func (conf Configuration) packFlags() (flags.WslFlags, error) {
300+
f := flags.NONE
312301

313302
if conf.InteropEnabled {
314-
flags = flags | flag_ENABLE_INTEROP
303+
f = f | flags.ENABLE_INTEROP
315304
}
316305

317306
if conf.PathAppended {
318-
flags = flags | flag_APPEND_NT_PATH
307+
f = f | flags.APPEND_NT_PATH
319308
}
320309

321310
if conf.DriveMountingEnabled {
322-
flags = flags | flag_ENABLE_DRIVE_MOUNTING
311+
f = f | flags.ENABLE_DRIVE_MOUNTING
323312
}
324313

325314
switch conf.undocumentedWSLVersion {
326315
case 1:
327316
case 2:
328-
flags = flags | flag_undocumented_WSL_VERSION
317+
f = f | flags.Undocumented_WSL_VERSION
329318
default:
330-
return flags, fmt.Errorf("unknown WSL version %d", conf.undocumentedWSLVersion)
319+
return f, fmt.Errorf("unknown WSL version %d", conf.undocumentedWSLVersion)
331320
}
332321

333-
return flags, nil
322+
return f, nil
334323
}

examples/demo.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import (
1212
)
1313

1414
func main() {
15-
distro := wsl.NewDistro("Ubuntu-GoWSL-demo")
15+
ctx := context.Background()
16+
17+
distro := wsl.NewDistro(ctx, "Ubuntu-GoWSL-demo")
1618

1719
// Registering a new distro
1820
fmt.Printf("Registering a new distro %q\n", distro.Name())
@@ -37,7 +39,7 @@ func main() {
3739
// programs such as bash, python, etc. it'll will start an interactive session
3840
// that the user can interact with. This is presumably what wsl.exe uses.
3941
// It is a blocking call.
40-
if err := distro.Shell(wsl.WithCommand("sh -c 'powershell.exe'")); err != nil {
42+
if err := distro.Shell(ctx, wsl.WithCommand("sh -c 'powershell.exe'")); err != nil {
4143
fmt.Printf("Interactive session unsuccesful: %v\n", err)
4244
} else {
4345
fmt.Println("Interactive session succesful")
@@ -89,7 +91,7 @@ func main() {
8991
fmt.Println("\nStarting a shell in Ubuntu. Feel free to `exit <NUMBER>` to continue the demo")
9092
fmt.Println("")
9193

92-
if err = distro.Shell(); err != nil {
94+
if err = distro.Shell(ctx); err != nil {
9395
fmt.Printf("Shell exited with an error: %v\n", err)
9496
} else {
9597
fmt.Println("Shell exited with no errors")

exec.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func (c *Cmd) Start() (err error) {
113113
}
114114
}
115115

116-
c.Process, err = WslLaunch(
116+
c.Process, err = selectBackend(c.ctx).WslLaunch(
117117
c.distro.Name(),
118118
c.command,
119119
c.UseCWD,
@@ -367,7 +367,7 @@ func (c *Cmd) readerDescriptor(r io.Reader) (f *os.File, err error) {
367367
}
368368

369369
if f, ok := r.(*os.File); ok {
370-
isPipe, err := IsPipe(f)
370+
isPipe, err := selectBackend(c.ctx).IsPipe(f)
371371
if err == nil && isPipe {
372372
// It's a pipe: no need to create our own pipe.
373373
return f, nil
@@ -410,7 +410,7 @@ func (c *Cmd) writerDescriptor(w io.Writer) (f *os.File, err error) {
410410
}
411411

412412
if f, ok := w.(*os.File); ok {
413-
isPipe, err := IsPipe(f)
413+
isPipe, err := selectBackend(c.ctx).IsPipe(f)
414414
if err == nil && isPipe {
415415
// It's a pipe: no need to create our own pipe.
416416
return f, nil

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ github.com/ubuntu/decorate v0.0.0-20230125165522-2d5b0a9bb117/go.mod h1:mx0Tjbqs
2525
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
2626
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
2727
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
28+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
2829
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
2930
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
3031
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

internal/flags/flags.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Package flags contains the enum used by WSL to display
2+
// some configuration of a WSL distro.
3+
//
4+
// TODO: This can probably be moved to the back-end.
5+
package flags
6+
7+
// WslFlags is an alias for Windows' WSL_DISTRIBUTION_FLAGS
8+
// https://learn.microsoft.com/en-us/windows/win32/api/wslapi/ne-wslapi-wsl_distribution_flags
9+
type WslFlags int32
10+
11+
// Allowing underscores in names to keep it as close to Windows as possible.
12+
const (
13+
// All nolints are regarding the use of UPPPER_CASE.
14+
15+
NONE WslFlags = 0x0
16+
ENABLE_INTEROP WslFlags = 0x1 //nolint: revive
17+
APPEND_NT_PATH WslFlags = 0x2 //nolint: revive
18+
ENABLE_DRIVE_MOUNTING WslFlags = 0x4 //nolint: revive
19+
20+
// Per the conversation at https://github.com/microsoft/WSL-DistroLauncher/issues/96
21+
// the information about version 1 or 2 is on the 4th bit of the flags, which is
22+
// currently referenced neither by the API nor the documentation.
23+
Undocumented_WSL_VERSION WslFlags = 0x8 //nolint: revive
24+
)

internal/interfaces/interfaces.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Package interfaces defines all the actions that a back-end to GoWSL must
2+
// be able to perform in order to run, or otherwise mock WSL.
3+
package interfaces
4+
5+
import (
6+
"os"
7+
8+
"github.com/ubuntu/gowsl/internal/flags"
9+
)
10+
11+
// Backend defines what a back-end to GoWSL must be able to do or mock.
12+
type Backend interface {
13+
// Registry
14+
OpenLxssRegistry(path ...string) (RegistryKey, error)
15+
16+
// wsl.exe
17+
Shutdown() error
18+
Terminate(distroName string) error
19+
SetAsDefault(distroName string) error
20+
21+
// Win32
22+
IsPipe(f *os.File) (bool, error)
23+
WslConfigureDistribution(distributionName string, defaultUID uint32, wslDistributionFlags flags.WslFlags) error
24+
WslGetDistributionConfiguration(distroName string, distributionVersion *uint8, defaultUID *uint32, wslDistributionFlags *flags.WslFlags, defaultEnvironmentVariables *map[string]string) error
25+
WslLaunch(distroName string, command string, useCWD bool, stdin *os.File, stdout *os.File, stderr *os.File) (*os.Process, error)
26+
WslLaunchInteractive(distributionName string, command string, useCurrentWorkingDirectory bool) (uint32, error)
27+
WslRegisterDistribution(distributionName string, tarGzFilename string) error
28+
WslUnregisterDistribution(distributionName string) error
29+
}
30+
31+
// RegistryKey mocks a very small subset of behaviours of a Windows Registry key, enough
32+
// for GoWSL to do the limited amount of traversal and reading that it needs.
33+
type RegistryKey interface {
34+
Close() error
35+
Field(name string) (string, error)
36+
SubkeyNames() ([]string, error)
37+
}

internal/mock/backend.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build mock
2+
3+
// Package mock mocks the WSL api, useful for tests as it allows parallelism,
4+
// decoupling, and execution speed.
5+
package mock
6+
7+
// Backend implements the Backend interface.
8+
type Backend struct{}

0 commit comments

Comments
 (0)