fusego/samples/subprocess.go

293 lines
6.6 KiB
Go
Raw Normal View History

2015-03-23 08:15:53 +03:00
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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.
package samples
import (
2015-03-24 01:22:39 +03:00
"bytes"
2015-03-23 08:26:15 +03:00
"fmt"
2015-03-23 08:15:53 +03:00
"io"
2015-03-23 08:26:15 +03:00
"io/ioutil"
"log"
2015-03-24 01:46:05 +03:00
"os"
2015-03-23 08:26:15 +03:00
"os/exec"
2015-03-23 08:31:52 +03:00
"path"
"sync"
2015-03-23 08:15:53 +03:00
2015-03-23 08:26:15 +03:00
"github.com/jacobsa/ogletest"
2015-03-23 08:15:53 +03:00
"golang.org/x/net/context"
)
// A struct that implements common behavior needed by tests in the samples/
// directory where the file system is mounted by a subprocess. Use it as an
// embedded field in your test fixture, calling its SetUp method from your
// SetUp method after setting the MountType and MountFlags fields.
type SubprocessTest struct {
// The type of the file system to mount. Must be recognized by mount_sample.
MountType string
// Additional flags to be passed to the mount_sample tool.
MountFlags []string
2015-03-24 01:46:05 +03:00
// A list of files to pass to mount_sample. The given string flag will be
// used to pass the file descriptor number.
MountFiles map[string]*os.File
2015-03-23 08:15:53 +03:00
// A context object that can be used for long-running operations.
Ctx context.Context
// The directory at which the file system is mounted.
Dir string
// Anothing non-nil in this slice will be closed by TearDown. The test will
// fail if closing fails.
ToClose []io.Closer
2015-03-24 01:59:13 +03:00
mountSampleErr <-chan error
2015-03-23 08:26:15 +03:00
}
// Mount the file system and initialize the other exported fields of the
// struct. Panics on error.
//
// REQUIRES: t.FileSystem has been set.
func (t *SubprocessTest) SetUp(ti *ogletest.TestInfo) {
err := t.initialize()
if err != nil {
panic(err)
}
}
2015-03-23 08:31:52 +03:00
// Set by buildMountSample.
var mountSamplePath string
var mountSampleErr error
var mountSampleOnce sync.Once
2015-03-23 08:26:15 +03:00
// Build the mount_sample tool if it has not yet been built for this process.
// Return a path to the binary.
2015-03-23 08:31:52 +03:00
func buildMountSample() (toolPath string, err error) {
// Build if we haven't yet.
mountSampleOnce.Do(func() {
// Create a temporary directory.
tempDir, err := ioutil.TempDir("", "")
if err != nil {
mountSampleErr = fmt.Errorf("TempDir: %v", err)
return
}
mountSamplePath = path.Join(tempDir, "mount_sample")
// Build the command.
cmd := exec.Command(
"go",
"build",
"-o",
2015-03-24 00:53:52 +03:00
mountSamplePath,
"github.com/jacobsa/fuse/samples/mount_sample")
2015-03-23 08:31:52 +03:00
output, err := cmd.CombinedOutput()
if err != nil {
mountSampleErr = fmt.Errorf(
"mount_sample exited with %v, output:\n%s",
err,
string(output))
return
}
})
if mountSampleErr != nil {
err = mountSampleErr
return
}
toolPath = mountSamplePath
return
}
2015-03-23 08:26:15 +03:00
2015-03-24 01:59:13 +03:00
func waitForMountSample(
cmd *exec.Cmd,
errChan chan<- error,
stderr *bytes.Buffer) {
// However we exit, write the error to the channel.
var err error
defer func() {
errChan <- err
}()
// Wait for the command.
err = cmd.Wait()
if err == nil {
return
}
// Make exit errors nicer.
if exitErr, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf(
"mount_sample exited with %v. Stderr:\n%s",
exitErr,
stderr.String())
return
}
err = fmt.Errorf("Waiting for mount_sample: %v", err)
}
2015-03-24 02:08:17 +03:00
func waitForReady(readyReader *os.File, c chan<- struct{}) {
_, err := readyReader.Read(make([]byte, 1))
if err != nil {
log.Printf("Readying from ready pipe: %v", err)
return
}
c <- struct{}{}
}
2015-03-23 08:26:15 +03:00
// Like SetUp, but doens't panic.
func (t *SubprocessTest) initialize() (err error) {
// Initialize the context.
t.Ctx = context.Background()
// Set up a temporary directory.
t.Dir, err = ioutil.TempDir("", "sample_test")
if err != nil {
err = fmt.Errorf("TempDir: %v", err)
return
}
// Build the mount_sample tool.
toolPath, err := buildMountSample()
if err != nil {
err = fmt.Errorf("buildMountSample: %v", err)
return
}
2015-03-24 01:46:05 +03:00
// Set up basic args for the subprocess.
2015-03-24 01:22:39 +03:00
args := []string{
"--type",
t.MountType,
2015-03-24 01:27:09 +03:00
"--mount_point",
t.Dir,
2015-03-24 01:22:39 +03:00
}
2015-03-23 08:26:15 +03:00
args = append(args, t.MountFlags...)
2015-03-24 02:08:17 +03:00
// Set up a pipe for the "ready" status.
readyReader, readyWriter, err := os.Pipe()
if err != nil {
err = fmt.Errorf("Pipe: %v", err)
return
}
defer readyReader.Close()
defer readyWriter.Close()
t.MountFiles["ready_file"] = readyWriter
2015-03-24 01:46:05 +03:00
// Set up inherited files and appropriate flags.
var extraFiles []*os.File
for flag, file := range t.MountFiles {
// Cf. os/exec.Cmd.ExtraFiles
fd := 3 + len(extraFiles)
extraFiles = append(extraFiles, file)
args = append(args, "--"+flag)
args = append(args, fmt.Sprintf("%d", fd))
}
// Set up a command.
2015-03-24 01:59:13 +03:00
var stderr bytes.Buffer
mountCmd := exec.Command(toolPath, args...)
mountCmd.Stderr = &stderr
mountCmd.ExtraFiles = extraFiles
2015-03-24 01:22:39 +03:00
// Start it.
2015-03-24 01:59:13 +03:00
if err = mountCmd.Start(); err != nil {
2015-03-24 01:22:39 +03:00
err = fmt.Errorf("mountCmd.Start: %v", err)
2015-03-23 08:26:15 +03:00
return
}
2015-03-24 01:59:13 +03:00
// Launch a goroutine that waits for it and returns its status.
mountSampleErr := make(chan error, 1)
go waitForMountSample(mountCmd, mountSampleErr, &stderr)
2015-03-24 02:08:17 +03:00
// Wait for the tool to say the file system is ready. In parallel, watch for
// the tool to fail.
readyChan := make(chan struct{}, 1)
go waitForReady(readyReader, readyChan)
select {
case <-readyChan:
case err = <-mountSampleErr:
return
}
// TearDown is no responsible for joining.
t.mountSampleErr = mountSampleErr
2015-03-23 08:26:15 +03:00
return
}
// Unmount the file system and clean up. Panics on error.
func (t *SubprocessTest) TearDown() {
err := t.destroy()
if err != nil {
panic(err)
}
}
// Like TearDown, but doesn't panic.
func (t *SubprocessTest) destroy() (err error) {
// Close what is necessary.
for _, c := range t.ToClose {
if c == nil {
continue
}
ogletest.ExpectEq(nil, c.Close())
}
// If we didn't try to mount the file system, there's nothing further to do.
2015-03-24 01:59:13 +03:00
if t.mountSampleErr == nil {
2015-03-23 08:26:15 +03:00
return
}
// In the background, initiate an unmount.
unmountErrChan := make(chan error)
go func() {
unmountErrChan <- unmount(t.Dir)
}()
// Make sure we wait for the unmount, even if we've already returned early in
// error. Return its error if we haven't seen any other error.
defer func() {
unmountErr := <-unmountErrChan
if unmountErr != nil {
if err != nil {
log.Println("unmount:", unmountErr)
return
}
err = fmt.Errorf("unmount: %v", unmountErr)
}
}()
2015-03-23 08:26:15 +03:00
// Wait for the subprocess.
2015-03-24 01:59:13 +03:00
if err = <-t.mountSampleErr; err != nil {
2015-03-23 08:26:15 +03:00
return
}
return
2015-03-23 08:15:53 +03:00
}