Kaynağa Gözat

Move integration tests to integration/, expose missing public methods in the core

Solomon Hykes 11 yıl önce
ebeveyn
işleme
359a6f49b9

+ 15 - 1
api.go

@@ -927,7 +927,7 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ
 		if err != nil {
 			return err
 		}
-		c, err := mkBuildContext(string(dockerFile), nil)
+		c, err := MkBuildContext(string(dockerFile), nil)
 		if err != nil {
 			return err
 		}
@@ -1105,6 +1105,20 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) {
 	return r, nil
 }
 
+// ServeRequest processes a single http request to the docker remote api.
+// FIXME: refactor this to be part of Server and not require re-creating a new
+// router each time. This requires first moving ListenAndServe into Server.
+func ServeRequest(srv *Server, apiversion float64, w http.ResponseWriter, req *http.Request) error {
+	router, err := createRouter(srv, false)
+	if err != nil {
+		return err
+	}
+	// Insert APIVERSION into the request as a convenience
+	req.URL.Path = fmt.Sprintf("/v%g%s", apiversion, req.URL.Path)
+	router.ServeHTTP(w, req)
+	return nil
+}
+
 func ListenAndServe(proto, addr string, srv *Server, logging bool) error {
 	log.Printf("Listening for HTTP on %s (%s)\n", addr, proto)
 

+ 2 - 2
commands.go

@@ -135,7 +135,7 @@ func (cli *DockerCli) CmdInsert(args ...string) error {
 
 // mkBuildContext returns an archive of an empty context with the contents
 // of `dockerfile` at the path ./Dockerfile
-func mkBuildContext(dockerfile string, files [][2]string) (archive.Archive, error) {
+func MkBuildContext(dockerfile string, files [][2]string) (archive.Archive, error) {
 	buf := new(bytes.Buffer)
 	tw := tar.NewWriter(buf)
 	files = append(files, [2]string{"Dockerfile", dockerfile})
@@ -185,7 +185,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 		if err != nil {
 			return err
 		}
-		context, err = mkBuildContext(string(dockerfile), nil)
+		context, err = MkBuildContext(string(dockerfile), nil)
 	} else if utils.IsURL(cmd.Arg(0)) || utils.IsGIT(cmd.Arg(0)) {
 		isRemote = true
 	} else {

+ 149 - 0
config_test.go

@@ -0,0 +1,149 @@
+package docker
+
+import (
+	"testing"
+)
+
+func TestCompareConfig(t *testing.T) {
+	volumes1 := make(map[string]struct{})
+	volumes1["/test1"] = struct{}{}
+	config1 := Config{
+		Dns:         []string{"1.1.1.1", "2.2.2.2"},
+		PortSpecs:   []string{"1111:1111", "2222:2222"},
+		Env:         []string{"VAR1=1", "VAR2=2"},
+		VolumesFrom: "11111111",
+		Volumes:     volumes1,
+	}
+	config2 := Config{
+		Dns:         []string{"0.0.0.0", "2.2.2.2"},
+		PortSpecs:   []string{"1111:1111", "2222:2222"},
+		Env:         []string{"VAR1=1", "VAR2=2"},
+		VolumesFrom: "11111111",
+		Volumes:     volumes1,
+	}
+	config3 := Config{
+		Dns:         []string{"1.1.1.1", "2.2.2.2"},
+		PortSpecs:   []string{"0000:0000", "2222:2222"},
+		Env:         []string{"VAR1=1", "VAR2=2"},
+		VolumesFrom: "11111111",
+		Volumes:     volumes1,
+	}
+	config4 := Config{
+		Dns:         []string{"1.1.1.1", "2.2.2.2"},
+		PortSpecs:   []string{"0000:0000", "2222:2222"},
+		Env:         []string{"VAR1=1", "VAR2=2"},
+		VolumesFrom: "22222222",
+		Volumes:     volumes1,
+	}
+	volumes2 := make(map[string]struct{})
+	volumes2["/test2"] = struct{}{}
+	config5 := Config{
+		Dns:         []string{"1.1.1.1", "2.2.2.2"},
+		PortSpecs:   []string{"0000:0000", "2222:2222"},
+		Env:         []string{"VAR1=1", "VAR2=2"},
+		VolumesFrom: "11111111",
+		Volumes:     volumes2,
+	}
+	if CompareConfig(&config1, &config2) {
+		t.Fatalf("CompareConfig should return false, Dns are different")
+	}
+	if CompareConfig(&config1, &config3) {
+		t.Fatalf("CompareConfig should return false, PortSpecs are different")
+	}
+	if CompareConfig(&config1, &config4) {
+		t.Fatalf("CompareConfig should return false, VolumesFrom are different")
+	}
+	if CompareConfig(&config1, &config5) {
+		t.Fatalf("CompareConfig should return false, Volumes are different")
+	}
+	if !CompareConfig(&config1, &config1) {
+		t.Fatalf("CompareConfig should return true")
+	}
+}
+
+func TestMergeConfig(t *testing.T) {
+	volumesImage := make(map[string]struct{})
+	volumesImage["/test1"] = struct{}{}
+	volumesImage["/test2"] = struct{}{}
+	configImage := &Config{
+		Dns:         []string{"1.1.1.1", "2.2.2.2"},
+		PortSpecs:   []string{"1111:1111", "2222:2222"},
+		Env:         []string{"VAR1=1", "VAR2=2"},
+		VolumesFrom: "1111",
+		Volumes:     volumesImage,
+	}
+
+	volumesUser := make(map[string]struct{})
+	volumesUser["/test3"] = struct{}{}
+	configUser := &Config{
+		Dns:       []string{"3.3.3.3"},
+		PortSpecs: []string{"3333:2222", "3333:3333"},
+		Env:       []string{"VAR2=3", "VAR3=3"},
+		Volumes:   volumesUser,
+	}
+
+	if err := MergeConfig(configUser, configImage); err != nil {
+		t.Error(err)
+	}
+
+	if len(configUser.Dns) != 3 {
+		t.Fatalf("Expected 3 dns, 1.1.1.1, 2.2.2.2 and 3.3.3.3, found %d", len(configUser.Dns))
+	}
+	for _, dns := range configUser.Dns {
+		if dns != "1.1.1.1" && dns != "2.2.2.2" && dns != "3.3.3.3" {
+			t.Fatalf("Expected 1.1.1.1 or 2.2.2.2 or 3.3.3.3, found %s", dns)
+		}
+	}
+
+	if len(configUser.ExposedPorts) != 3 {
+		t.Fatalf("Expected 3 ExposedPorts, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts))
+	}
+	for portSpecs := range configUser.ExposedPorts {
+		if portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" {
+			t.Fatalf("Expected 1111 or 2222 or 3333, found %s", portSpecs)
+		}
+	}
+	if len(configUser.Env) != 3 {
+		t.Fatalf("Expected 3 env var, VAR1=1, VAR2=3 and VAR3=3, found %d", len(configUser.Env))
+	}
+	for _, env := range configUser.Env {
+		if env != "VAR1=1" && env != "VAR2=3" && env != "VAR3=3" {
+			t.Fatalf("Expected VAR1=1 or VAR2=3 or VAR3=3, found %s", env)
+		}
+	}
+
+	if len(configUser.Volumes) != 3 {
+		t.Fatalf("Expected 3 volumes, /test1, /test2 and /test3, found %d", len(configUser.Volumes))
+	}
+	for v := range configUser.Volumes {
+		if v != "/test1" && v != "/test2" && v != "/test3" {
+			t.Fatalf("Expected /test1 or /test2 or /test3, found %s", v)
+		}
+	}
+
+	if configUser.VolumesFrom != "1111" {
+		t.Fatalf("Expected VolumesFrom to be 1111, found %s", configUser.VolumesFrom)
+	}
+
+	ports, _, err := parsePortSpecs([]string{"0000"})
+	if err != nil {
+		t.Error(err)
+	}
+	configImage2 := &Config{
+		ExposedPorts: ports,
+	}
+
+	if err := MergeConfig(configUser, configImage2); err != nil {
+		t.Error(err)
+	}
+
+	if len(configUser.ExposedPorts) != 4 {
+		t.Fatalf("Expected 4 ExposedPorts, 0000, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts))
+	}
+	for portSpecs := range configUser.ExposedPorts {
+		if portSpecs.Port() != "0000" && portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" {
+			t.Fatalf("Expected 0000 or 1111 or 2222 or 3333, found %s", portSpecs)
+		}
+	}
+
+}

+ 257 - 0
container_unit_test.go

@@ -0,0 +1,257 @@
+package docker
+
+import (
+	"testing"
+)
+
+
+
+func TestParseLxcConfOpt(t *testing.T) {
+	opts := []string{"lxc.utsname=docker", "lxc.utsname = docker "}
+
+	for _, o := range opts {
+		k, v, err := parseLxcOpt(o)
+		if err != nil {
+			t.FailNow()
+		}
+		if k != "lxc.utsname" {
+			t.Fail()
+		}
+		if v != "docker" {
+			t.Fail()
+		}
+	}
+}
+
+func TestParseNetworkOptsPrivateOnly(t *testing.T) {
+	ports, bindings, err := parsePortSpecs([]string{"192.168.1.100::80"})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(ports) != 1 {
+		t.Logf("Expected 1 got %d", len(ports))
+		t.FailNow()
+	}
+	if len(bindings) != 1 {
+		t.Logf("Expected 1 got %d", len(bindings))
+		t.FailNow()
+	}
+	for k := range ports {
+		if k.Proto() != "tcp" {
+			t.Logf("Expected tcp got %s", k.Proto())
+			t.Fail()
+		}
+		if k.Port() != "80" {
+			t.Logf("Expected 80 got %s", k.Port())
+			t.Fail()
+		}
+		b, exists := bindings[k]
+		if !exists {
+			t.Log("Binding does not exist")
+			t.FailNow()
+		}
+		if len(b) != 1 {
+			t.Logf("Expected 1 got %d", len(b))
+			t.FailNow()
+		}
+		s := b[0]
+		if s.HostPort != "" {
+			t.Logf("Expected \"\" got %s", s.HostPort)
+			t.Fail()
+		}
+		if s.HostIp != "192.168.1.100" {
+			t.Fail()
+		}
+	}
+}
+
+func TestParseNetworkOptsPublic(t *testing.T) {
+	ports, bindings, err := parsePortSpecs([]string{"192.168.1.100:8080:80"})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(ports) != 1 {
+		t.Logf("Expected 1 got %d", len(ports))
+		t.FailNow()
+	}
+	if len(bindings) != 1 {
+		t.Logf("Expected 1 got %d", len(bindings))
+		t.FailNow()
+	}
+	for k := range ports {
+		if k.Proto() != "tcp" {
+			t.Logf("Expected tcp got %s", k.Proto())
+			t.Fail()
+		}
+		if k.Port() != "80" {
+			t.Logf("Expected 80 got %s", k.Port())
+			t.Fail()
+		}
+		b, exists := bindings[k]
+		if !exists {
+			t.Log("Binding does not exist")
+			t.FailNow()
+		}
+		if len(b) != 1 {
+			t.Logf("Expected 1 got %d", len(b))
+			t.FailNow()
+		}
+		s := b[0]
+		if s.HostPort != "8080" {
+			t.Logf("Expected 8080 got %s", s.HostPort)
+			t.Fail()
+		}
+		if s.HostIp != "192.168.1.100" {
+			t.Fail()
+		}
+	}
+}
+
+func TestParseNetworkOptsUdp(t *testing.T) {
+	ports, bindings, err := parsePortSpecs([]string{"192.168.1.100::6000/udp"})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(ports) != 1 {
+		t.Logf("Expected 1 got %d", len(ports))
+		t.FailNow()
+	}
+	if len(bindings) != 1 {
+		t.Logf("Expected 1 got %d", len(bindings))
+		t.FailNow()
+	}
+	for k := range ports {
+		if k.Proto() != "udp" {
+			t.Logf("Expected udp got %s", k.Proto())
+			t.Fail()
+		}
+		if k.Port() != "6000" {
+			t.Logf("Expected 6000 got %s", k.Port())
+			t.Fail()
+		}
+		b, exists := bindings[k]
+		if !exists {
+			t.Log("Binding does not exist")
+			t.FailNow()
+		}
+		if len(b) != 1 {
+			t.Logf("Expected 1 got %d", len(b))
+			t.FailNow()
+		}
+		s := b[0]
+		if s.HostPort != "" {
+			t.Logf("Expected \"\" got %s", s.HostPort)
+			t.Fail()
+		}
+		if s.HostIp != "192.168.1.100" {
+			t.Fail()
+		}
+	}
+}
+
+
+
+
+// FIXME: test that destroying a container actually removes its root directory
+
+
+/*
+func TestLXCConfig(t *testing.T) {
+	// Memory is allocated randomly for testing
+	rand.Seed(time.Now().UTC().UnixNano())
+	memMin := 33554432
+	memMax := 536870912
+	mem := memMin + rand.Intn(memMax-memMin)
+	// CPU shares as well
+	cpuMin := 100
+	cpuMax := 10000
+	cpu := cpuMin + rand.Intn(cpuMax-cpuMin)
+	container, _, err := runtime.Create(&Config{
+		Image: GetTestImage(runtime).ID,
+		Cmd:   []string{"/bin/true"},
+
+		Hostname:  "foobar",
+		Memory:    int64(mem),
+		CpuShares: int64(cpu),
+	},
+		"",
+	)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer runtime.Destroy(container)
+	container.generateLXCConfig()
+	grepFile(t, container.lxcConfigPath(), "lxc.utsname = foobar")
+	grepFile(t, container.lxcConfigPath(),
+		fmt.Sprintf("lxc.cgroup.memory.limit_in_bytes = %d", mem))
+	grepFile(t, container.lxcConfigPath(),
+		fmt.Sprintf("lxc.cgroup.memory.memsw.limit_in_bytes = %d", mem*2))
+}
+
+
+func TestCustomLxcConfig(t *testing.T) {
+	runtime := mkRuntime(t)
+	defer nuke(runtime)
+	container, _, err := runtime.Create(&Config{
+		Image: GetTestImage(runtime).ID,
+		Cmd:   []string{"/bin/true"},
+
+		Hostname: "foobar",
+	},
+		"",
+	)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer runtime.Destroy(container)
+	container.hostConfig = &HostConfig{LxcConf: []KeyValuePair{
+		{
+			Key:   "lxc.utsname",
+			Value: "docker",
+		},
+		{
+			Key:   "lxc.cgroup.cpuset.cpus",
+			Value: "0,1",
+		},
+	}}
+
+	container.generateLXCConfig()
+	grepFile(t, container.lxcConfigPath(), "lxc.utsname = docker")
+	grepFile(t, container.lxcConfigPath(), "lxc.cgroup.cpuset.cpus = 0,1")
+}
+
+
+func grepFile(t *testing.T, path string, pattern string) {
+	f, err := os.Open(path)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer f.Close()
+	r := bufio.NewReader(f)
+	var (
+		line string
+	)
+	err = nil
+	for err == nil {
+		line, err = r.ReadString('\n')
+		if strings.Contains(line, pattern) == true {
+			return
+		}
+	}
+	t.Fatalf("grepFile: pattern \"%s\" not found in \"%s\"", pattern, path)
+}
+*/
+
+
+func TestGetFullName(t *testing.T) {
+	name, err := getFullName("testing")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if name != "/testing" {
+		t.Fatalf("Expected /testing got %s", name)
+	}
+	if _, err := getFullName(""); err == nil {
+		t.Fatal("Error should not be nil")
+	}
+}

+ 5 - 1
engine/job.go

@@ -214,7 +214,7 @@ func (job *Job) GetenvList(key string) []string {
 	return l
 }
 
-func (job *Job) SetenvList(key string, value []string) error {
+func (job *Job) SetenvJson(key string, value interface{}) error {
 	sval, err := json.Marshal(value)
 	if err != nil {
 		return err
@@ -223,6 +223,10 @@ func (job *Job) SetenvList(key string, value []string) error {
 	return nil
 }
 
+func (job *Job) SetenvList(key string, value []string) error {
+	return job.SetenvJson(key, value)
+}
+
 func (job *Job) Setenv(key, value string) {
 	job.env = append(job.env, key+"="+value)
 }

+ 52 - 0
http_test.go

@@ -0,0 +1,52 @@
+package docker
+
+import (
+	"testing"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+)
+
+
+func TestGetBoolParam(t *testing.T) {
+	if ret, err := getBoolParam("true"); err != nil || !ret {
+		t.Fatalf("true -> true, nil | got %t %s", ret, err)
+	}
+	if ret, err := getBoolParam("True"); err != nil || !ret {
+		t.Fatalf("True -> true, nil | got %t %s", ret, err)
+	}
+	if ret, err := getBoolParam("1"); err != nil || !ret {
+		t.Fatalf("1 -> true, nil | got %t %s", ret, err)
+	}
+	if ret, err := getBoolParam(""); err != nil || ret {
+		t.Fatalf("\"\" -> false, nil | got %t %s", ret, err)
+	}
+	if ret, err := getBoolParam("false"); err != nil || ret {
+		t.Fatalf("false -> false, nil | got %t %s", ret, err)
+	}
+	if ret, err := getBoolParam("0"); err != nil || ret {
+		t.Fatalf("0 -> false, nil | got %t %s", ret, err)
+	}
+	if ret, err := getBoolParam("faux"); err == nil || ret {
+		t.Fatalf("faux -> false, err | got %t %s", ret, err)
+	}
+}
+
+func TesthttpError(t *testing.T) {
+	r := httptest.NewRecorder()
+
+	httpError(r, fmt.Errorf("No such method"))
+	if r.Code != http.StatusNotFound {
+		t.Fatalf("Expected %d, got %d", http.StatusNotFound, r.Code)
+	}
+
+	httpError(r, fmt.Errorf("This accound hasn't been activated"))
+	if r.Code != http.StatusForbidden {
+		t.Fatalf("Expected %d, got %d", http.StatusForbidden, r.Code)
+	}
+
+	httpError(r, fmt.Errorf("Some error"))
+	if r.Code != http.StatusInternalServerError {
+		t.Fatalf("Expected %d, got %d", http.StatusInternalServerError, r.Code)
+	}
+}

+ 0 - 0
api_test.go → integration/api_test.go


+ 0 - 0
buildfile_test.go → integration/buildfile_test.go


+ 0 - 0
commands_test.go → integration/commands_test.go


+ 0 - 0
container_test.go → integration/container_test.go


+ 0 - 0
runtime_test.go → integration/runtime_test.go


+ 0 - 0
server_test.go → integration/server_test.go


+ 0 - 0
sorter_test.go → integration/sorter_test.go


+ 0 - 0
utils_test.go → integration/utils_test.go


+ 0 - 0
z_final_test.go → integration/z_final_test.go


+ 34 - 0
runtime.go

@@ -15,6 +15,7 @@ import (
 	"path"
 	"sort"
 	"strings"
+	"sync"
 	"time"
 )
 
@@ -516,7 +517,12 @@ func (runtime *Runtime) Commit(container *Container, repository, tag, comment, a
 	return img, nil
 }
 
+// FIXME: this is deprecated by the getFullName *function*
 func (runtime *Runtime) getFullName(name string) (string, error) {
+	return getFullName(name)
+}
+
+func getFullName(name string) (string, error) {
 	if name == "" {
 		return "", fmt.Errorf("Container name cannot be empty")
 	}
@@ -655,6 +661,26 @@ func (runtime *Runtime) Close() error {
 	return runtime.containerGraph.Close()
 }
 
+// Nuke kills all containers then removes all content
+// from the content root, including images, volumes and
+// container filesystems.
+// Again: this will remove your entire docker runtime!
+func (runtime *Runtime) Nuke() error {
+	var wg sync.WaitGroup
+	for _, container := range runtime.List() {
+		wg.Add(1)
+		go func(c *Container) {
+			c.Kill()
+			wg.Done()
+		}(container)
+	}
+	wg.Wait()
+	runtime.Close()
+
+	return os.RemoveAll(runtime.config.Root)
+}
+
+
 func linkLxcStart(root string) error {
 	sourcePath, err := exec.LookPath("lxc-start")
 	if err != nil {
@@ -672,6 +698,14 @@ func linkLxcStart(root string) error {
 	return os.Symlink(sourcePath, targetPath)
 }
 
+// FIXME: this is a convenience function for integration tests
+// which need direct access to runtime.graph.
+// Once the tests switch to using engine and jobs, this method
+// can go away.
+func (runtime *Runtime) Graph() *Graph {
+	return runtime.graph
+}
+
 // History is a convenience type for storing a list of containers,
 // ordered by creation date.
 type History []*Container

+ 11 - 2
server.go

@@ -62,6 +62,8 @@ func jobInitApi(job *engine.Job) string {
 		os.Exit(0)
 	}()
 	job.Eng.Hack_SetGlobalVar("httpapi.server", srv)
+	job.Eng.Hack_SetGlobalVar("httpapi.runtime", srv.runtime)
+	job.Eng.Hack_SetGlobalVar("httpapi.bridgeIP", srv.runtime.networkManager.bridgeNetwork.IP)
 	if err := job.Eng.Register("create", srv.ContainerCreate); err != nil {
 		return err.Error()
 	}
@@ -530,6 +532,7 @@ func (srv *Server) ContainerCommit(name, repo, tag, author, comment string, conf
 	return img.ID, err
 }
 
+// FIXME: this should be called ImageTag
 func (srv *Server) ContainerTag(name, repo, tag string, force bool) error {
 	if err := srv.runtime.repositories.Set(repo, tag, name, force); err != nil {
 		return err
@@ -1062,7 +1065,12 @@ func (srv *Server) ContainerCreate(job *engine.Job) string {
 		return err.Error()
 	}
 	srv.LogEvent("create", container.ID, srv.runtime.repositories.ImageName(container.Image))
-	job.Printf("%s\n", container.ID)
+	// FIXME: this is necessary because runtime.Create might return a nil container
+	// with a non-nil error. This should not happen! Once it's fixed we
+	// can remove this workaround.
+	if container != nil {
+		job.Printf("%s\n", container.ID)
+	}
 	for _, warning := range buildWarnings {
 		job.Errorf("%s\n", warning)
 	}
@@ -1600,7 +1608,7 @@ func (srv *Server) HTTPRequestFactory(metaHeaders map[string][]string) *utils.HT
 	return srv.reqFactory
 }
 
-func (srv *Server) LogEvent(action, id, from string) {
+func (srv *Server) LogEvent(action, id, from string) *utils.JSONMessage {
 	now := time.Now().Unix()
 	jm := utils.JSONMessage{Status: action, ID: id, From: from, Time: now}
 	srv.events = append(srv.events, jm)
@@ -1610,6 +1618,7 @@ func (srv *Server) LogEvent(action, id, from string) {
 		default:
 		}
 	}
+	return &jm
 }
 
 type Server struct {

+ 110 - 0
server_unit_test.go

@@ -0,0 +1,110 @@
+package docker
+
+import (
+	"github.com/dotcloud/docker/utils"
+	"testing"
+	"time"
+)
+
+func TestPools(t *testing.T) {
+	srv := &Server{
+		pullingPool: make(map[string]struct{}),
+		pushingPool: make(map[string]struct{}),
+	}
+
+	err := srv.poolAdd("pull", "test1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = srv.poolAdd("pull", "test2")
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = srv.poolAdd("push", "test1")
+	if err == nil || err.Error() != "pull test1 is already in progress" {
+		t.Fatalf("Expected `pull test1 is already in progress`")
+	}
+	err = srv.poolAdd("pull", "test1")
+	if err == nil || err.Error() != "pull test1 is already in progress" {
+		t.Fatalf("Expected `pull test1 is already in progress`")
+	}
+	err = srv.poolAdd("wait", "test3")
+	if err == nil || err.Error() != "Unknown pool type" {
+		t.Fatalf("Expected `Unknown pool type`")
+	}
+
+	err = srv.poolRemove("pull", "test2")
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = srv.poolRemove("pull", "test2")
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = srv.poolRemove("pull", "test1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = srv.poolRemove("push", "test1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = srv.poolRemove("wait", "test3")
+	if err == nil || err.Error() != "Unknown pool type" {
+		t.Fatalf("Expected `Unknown pool type`")
+	}
+}
+
+
+func TestLogEvent(t *testing.T) {
+	srv := &Server{
+		events:    make([]utils.JSONMessage, 0, 64),
+		listeners: make(map[string]chan utils.JSONMessage),
+	}
+
+	srv.LogEvent("fakeaction", "fakeid", "fakeimage")
+
+	listener := make(chan utils.JSONMessage)
+	srv.Lock()
+	srv.listeners["test"] = listener
+	srv.Unlock()
+
+	srv.LogEvent("fakeaction2", "fakeid", "fakeimage")
+
+	if len(srv.events) != 2 {
+		t.Fatalf("Expected 2 events, found %d", len(srv.events))
+	}
+	go func() {
+		time.Sleep(200 * time.Millisecond)
+		srv.LogEvent("fakeaction3", "fakeid", "fakeimage")
+		time.Sleep(200 * time.Millisecond)
+		srv.LogEvent("fakeaction4", "fakeid", "fakeimage")
+	}()
+
+	setTimeout(t, "Listening for events timed out", 2*time.Second, func() {
+		for i := 2; i < 4; i++ {
+			event := <-listener
+			if event != srv.events[i] {
+				t.Fatalf("Event received it different than expected")
+			}
+		}
+	})
+}
+
+// FIXME: this is duplicated from integration/commands_test.go
+func setTimeout(t *testing.T, msg string, d time.Duration, f func()) {
+	c := make(chan bool)
+
+	// Make sure we are not too long
+	go func() {
+		time.Sleep(d)
+		c <- true
+	}()
+	go func() {
+		f()
+		c <- false
+	}()
+	if <-c && msg != "" {
+		t.Fatal(msg)
+	}
+}

+ 41 - 0
sorter_unit_test.go

@@ -0,0 +1,41 @@
+package docker
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestSortUniquePorts(t *testing.T) {
+	ports := []Port{
+		Port("6379/tcp"),
+		Port("22/tcp"),
+	}
+
+	sortPorts(ports, func(ip, jp Port) bool {
+		return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && ip.Proto() == "tcp")
+	})
+
+	first := ports[0]
+	if fmt.Sprint(first) != "22/tcp" {
+		t.Log(fmt.Sprint(first))
+		t.Fail()
+	}
+}
+
+func TestSortSamePortWithDifferentProto(t *testing.T) {
+	ports := []Port{
+		Port("8888/tcp"),
+		Port("8888/udp"),
+		Port("6379/tcp"),
+		Port("6379/udp"),
+	}
+
+	sortPorts(ports, func(ip, jp Port) bool {
+		return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && ip.Proto() == "tcp")
+	})
+
+	first := ports[0]
+	if fmt.Sprint(first) != "6379/tcp" {
+		t.Fail()
+	}
+}

+ 81 - 0
tags_unit_test.go

@@ -0,0 +1,81 @@
+package docker
+
+import (
+	"github.com/dotcloud/docker/utils"
+	"testing"
+	"path"
+	"os"
+)
+
+const (
+	testImageName string = "myapp"
+	testImageID   string = "foo"
+)
+
+func mkTestTagStore(root string, t *testing.T) *TagStore {
+	graph, err := NewGraph(root)
+	if err != nil {
+		t.Fatal(err)
+	}
+	store, err := NewTagStore(path.Join(root, "tags"), graph)
+	if err != nil {
+		t.Fatal(err)
+	}
+	archive, err := fakeTar()
+	if err != nil {
+		t.Fatal(err)
+	}
+	img := &Image{ID: testImageID}
+	if err := graph.Register(nil, archive, img); err != nil {
+		t.Fatal(err)
+	}
+	if err := store.Set(testImageName, "", testImageID, false); err != nil {
+		t.Fatal(err)
+	}
+	return store
+}
+
+
+func TestLookupImage(t *testing.T) {
+	tmp, err := utils.TestDirectory("")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmp)
+	store := mkTestTagStore(tmp, t)
+
+	if img, err := store.LookupImage(testImageName); err != nil {
+		t.Fatal(err)
+	} else if img == nil {
+		t.Errorf("Expected 1 image, none found")
+	}
+	if img, err := store.LookupImage(testImageName + ":" + DEFAULTTAG); err != nil {
+		t.Fatal(err)
+	} else if img == nil {
+		t.Errorf("Expected 1 image, none found")
+	}
+
+	if img, err := store.LookupImage(testImageName + ":" + "fail"); err == nil {
+		t.Errorf("Expected error, none found")
+	} else if img != nil {
+		t.Errorf("Expected 0 image, 1 found")
+	}
+
+	if img, err := store.LookupImage("fail:fail"); err == nil {
+		t.Errorf("Expected error, none found")
+	} else if img != nil {
+		t.Errorf("Expected 0 image, 1 found")
+	}
+
+	if img, err := store.LookupImage(testImageID); err != nil {
+		t.Fatal(err)
+	} else if img == nil {
+		t.Errorf("Expected 1 image, none found")
+	}
+
+	if img, err := store.LookupImage(testImageName + ":" + testImageID); err != nil {
+		t.Fatal(err)
+	} else if img == nil {
+		t.Errorf("Expected 1 image, none found")
+	}
+}

+ 40 - 0
utils/utils.go

@@ -1207,3 +1207,43 @@ func PartParser(template, data string) (map[string]string, error) {
 	}
 	return out, nil
 }
+
+
+
+var globalTestID string
+
+// TestDirectory creates a new temporary directory and returns its path.
+// The contents of directory at path `templateDir` is copied into the
+// new directory.
+func TestDirectory(templateDir string) (dir string, err error) {
+	if globalTestID == "" {
+		globalTestID = RandomString()[:4]
+	}
+	prefix := fmt.Sprintf("docker-test%s-%s-", globalTestID, GetCallerName(2))
+	if prefix == "" {
+		prefix = "docker-test-"
+	}
+	dir, err = ioutil.TempDir("", prefix)
+	if err = os.Remove(dir); err != nil {
+		return
+	}
+	if templateDir != "" {
+		if err = CopyDirectory(templateDir, dir); err != nil {
+			return
+		}
+	}
+	return
+}
+
+// GetCallerName introspects the call stack and returns the name of the
+// function `depth` levels down in the stack.
+func GetCallerName(depth int) string {
+	// Use the caller function name as a prefix.
+	// This helps trace temp directories back to their test.
+	pc, _, _, _ := runtime.Caller(depth + 1)
+	callerLongName := runtime.FuncForPC(pc).Name()
+	parts := strings.Split(callerLongName, ".")
+	callerShortName := parts[len(parts)-1]
+	return callerShortName
+}
+