package dockerfile
import (
"bytes"
"reflect"
"strings"
"testing"
"github.com/docker/docker/builder/dockerfile/command"
"github.com/docker/docker/builder/dockerfile/parser"
)
// TestParseTreeToDockerfile tests calling ParseTreeToDockerfile with multiple
// valid inputs.
func TestParseTreeToDockerfile(t *testing.T) {
testCases := map[string]struct {
in string
want string
}{
"empty input": {
in: ``,
want: ``,
},
"only comments": {
in: `# This is a comment
# and this is another comment
# while this is an indented comment`,
want: ``,
},
"simple Dockerfile": {
in: `FROM scratch
LABEL version=1.0
FROM busybox
ENV PATH=/bin
`,
want: `FROM scratch
LABEL version=1.0
FROM busybox
ENV PATH=/bin
`,
},
"Dockerfile with comments": {
in: `# This is a Dockerfile
FROM scratch
LABEL version=1.0
# Here we start building a second image
FROM busybox
ENV PATH=/bin
`,
want: `FROM scratch
LABEL version=1.0
FROM busybox
ENV PATH=/bin
`,
},
"all Dockerfile instructions": {
in: `FROM busybox:latest
MAINTAINER nobody@example.com
ONBUILD ADD . /app/src
ONBUILD RUN echo "Hello universe!"
LABEL version=1.0
EXPOSE 8080
VOLUME /var/run/www
ENV PATH=/bin
ADD file /home/
COPY dir/ /tmp/
RUN echo "Hello world!"
ENTRYPOINT /bin/sh
CMD ["-c", "env"]
USER 1001
WORKDIR /home
`,
want: `FROM busybox:latest
MAINTAINER nobody@example.com
ONBUILD ADD . /app/src
ONBUILD RUN echo "Hello universe!"
LABEL version=1.0
EXPOSE 8080
VOLUME /var/run/www
ENV PATH=/bin
ADD file /home/
COPY dir/ /tmp/
RUN echo "Hello world!"
ENTRYPOINT /bin/sh
CMD ["-c", "env"]
USER 1001
WORKDIR /home
`,
},
}
for name, tc := range testCases {
node, err := parser.Parse(strings.NewReader(tc.in))
if err != nil {
t.Errorf("%s: parse error: %v", name, err)
continue
}
got := ParseTreeToDockerfile(node)
want := []byte(tc.want)
if !reflect.DeepEqual(got, want) {
t.Errorf("ParseTreeToDockerfile: %s:\ngot:\n%swant:\n%s", name, got, want)
}
}
}
// TestParseTreeToDockerfileNilNode tests calling ParseTreeToDockerfile with a
// nil *parser.Node.
func TestParseTreeToDockerfileNilNode(t *testing.T) {
got := ParseTreeToDockerfile(nil)
if got != nil {
t.Errorf("ParseTreeToDockerfile(nil) = %#v; want nil", got)
}
}
// TestFindAll tests calling FindAll with multiple values of cmd.
func TestFindAll(t *testing.T) {
instructions := `FROM scratch
LABEL version=1.0
FROM busybox
ENV PATH=/bin
`
node, err := parser.Parse(strings.NewReader(instructions))
if err != nil {
t.Fatalf("parse error: %v", err)
}
for cmd, want := range map[string][]int{
command.From: {0, 2},
command.Label: {1},
command.Env: {3},
command.Maintainer: nil,
"UnknownCommand": nil,
} {
got := FindAll(node, cmd)
if !reflect.DeepEqual(got, want) {
t.Errorf("FindAll(node, %q) = %#v; want %#v", cmd, got, want)
}
}
}
// TestFindAllNilNode tests calling FindAll with a nil *parser.Node.
func TestFindAllNilNode(t *testing.T) {
cmd := command.From
got := FindAll(nil, cmd)
if got != nil {
t.Errorf("FindAll(nil, %q) = %#v; want nil", cmd, got)
}
}
// TestInsertInstructions tests calling InsertInstructions with multiple valid
// combinations of input.
func TestInsertInstructions(t *testing.T) {
testCases := map[string]struct {
original string
index int
newInstructions string
want string
}{
"insert nothing": {
original: `FROM busybox
ENV PATH=/bin
`,
index: 0,
newInstructions: ``,
want: `FROM busybox
ENV PATH=/bin
`,
},
"insert instruction in empty file": {
original: ``,
index: 0,
newInstructions: `FROM busybox`,
want: `FROM busybox
`,
},
"prepend single instruction": {
original: `FROM busybox
ENV PATH=/bin
`,
index: 0,
newInstructions: `FROM scratch`,
want: `FROM scratch
FROM busybox
ENV PATH=/bin
`,
},
"append single instruction": {
original: `FROM busybox
ENV PATH=/bin
`,
index: 2,
newInstructions: `FROM scratch`,
want: `FROM busybox
ENV PATH=/bin
FROM scratch
`,
},
"insert single instruction in the middle": {
original: `FROM busybox
ENV PATH=/bin
`,
index: 1,
newInstructions: `LABEL version=1.0`,
want: `FROM busybox
LABEL version=1.0
ENV PATH=/bin
`,
},
}
for name, tc := range testCases {
got, err := parser.Parse(strings.NewReader(tc.original))
if err != nil {
t.Errorf("InsertInstructions: %s: parse error: %v", name, err)
continue
}
err = InsertInstructions(got, tc.index, tc.newInstructions)
if err != nil {
t.Errorf("InsertInstructions: %s: %v", name, err)
continue
}
want, err := parser.Parse(strings.NewReader(tc.want))
if err != nil {
t.Errorf("InsertInstructions: %s: parse error: %v", name, err)
continue
}
if !bytes.Equal(ParseTreeToDockerfile(got), ParseTreeToDockerfile(want)) {
t.Errorf("InsertInstructions: %s: got %#v; want %#v", name, got, want)
}
}
}
// TestInsertInstructionsNilNode tests calling InsertInstructions with a nil
// *parser.Node.
func TestInsertInstructionsNilNode(t *testing.T) {
err := InsertInstructions(nil, 0, "")
if err == nil {
t.Errorf("InsertInstructions: got nil; want error")
}
}
// TestInsertInstructionsPosOutOfRange tests calling InsertInstructions with
// invalid values for the pos argument.
func TestInsertInstructionsPosOutOfRange(t *testing.T) {
original := `FROM busybox
ENV PATH=/bin
`
node, err := parser.Parse(strings.NewReader(original))
if err != nil {
t.Fatalf("parse error: %v", err)
}
for _, pos := range []int{-1, 3, 4} {
err := InsertInstructions(node, pos, "")
if err == nil {
t.Errorf("InsertInstructions(node, %d, \"\"): got nil; want error", pos)
}
}
}
// TestInsertInstructionsUnparseable tests calling InsertInstructions with
// instructions that the Docker parser cannot handle.
func TestInsertInstructionsUnparseable(t *testing.T) {
original := `FROM busybox
ENV PATH=/bin
`
node, err := parser.Parse(strings.NewReader(original))
if err != nil {
t.Fatalf("parse error: %v", err)
}
for name, instructions := range map[string]string{
"env without value": `ENV PATH`,
"nested json": `CMD [ "echo", [ "nested json" ] ]`,
} {
err = InsertInstructions(node, 1, instructions)
if err == nil {
t.Errorf("InsertInstructions: %s: got nil; want error", name)
}
}
}
// TestLastBaseImage tests calling LastBaseImage with multiple valid
// combinations of input.
func TestLastBaseImage(t *testing.T) {
testCases := map[string]struct {
in string
want string
}{
"empty Dockerfile": {
in: ``,
want: "",
},
"FROM missing argument": {
in: `FROM`,
want: "",
},
"single FROM": {
in: `FROM centos:7`,
want: "centos:7",
},
"multiple FROM": {
in: `FROM scratch
COPY . /boot
FROM centos:7`,
want: "centos:7",
},
}
for name, tc := range testCases {
node, err := parser.Parse(strings.NewReader(tc.in))
if err != nil {
t.Errorf("%s: parse error: %v", name, err)
continue
}
got := LastBaseImage(node)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("LastBaseImage: %s: got %#v; want %#v", name, got, tc.want)
}
}
}
// TestLastBaseImageNilNode tests calling LastBaseImage with a nil *parser.Node.
func TestLastBaseImageNilNode(t *testing.T) {
want := ""
if got := LastBaseImage(nil); got != want {
t.Errorf("LastBaseImage(nil) = %#v; want %#v", got, want)
}
}
// TestBaseImages tests calling baseImages with multiple valid combinations of
// input.
func TestBaseImages(t *testing.T) {
testCases := map[string]struct {
in string
want []string
}{
"empty Dockerfile": {
in: ``,
want: nil,
},
"FROM missing argument": {
in: `FROM`,
want: nil,
},
"single FROM": {
in: `FROM centos:7`,
want: []string{"centos:7"},
},
"multiple FROM": {
in: `FROM scratch
COPY . /boot
FROM centos:7`,
want: []string{"scratch", "centos:7"},
},
}
for name, tc := range testCases {
node, err := parser.Parse(strings.NewReader(tc.in))
if err != nil {
t.Errorf("%s: parse error: %v", name, err)
continue
}
got := baseImages(node)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("baseImages: %s: got %#v; want %#v", name, got, tc.want)
}
}
}
// TestBaseImagesNilNode tests calling baseImages with a nil *parser.Node.
func TestBaseImagesNilNode(t *testing.T) {
if got := baseImages(nil); got != nil {
t.Errorf("baseImages(nil) = %#v; want nil", got)
}
}
// TestLastExposedPorts tests calling LastExposedPorts with multiple valid
// combinations of input.
func TestLastExposedPorts(t *testing.T) {
testCases := map[string]struct {
in string
want []string
}{
"empty Dockerfile": {
in: ``,
want: nil,
},
"EXPOSE missing argument": {
in: `EXPOSE`,
want: nil,
},
"EXPOSE no FROM": {
in: `EXPOSE 8080`,
want: nil,
},
"single EXPOSE after FROM": {
in: `FROM centos:7
EXPOSE 8080`,
want: []string{"8080"},
},
"multiple EXPOSE and FROM": {
in: `# EXPOSE before FROM should be ignore
EXPOSE 777
FROM busybox
EXPOSE 8080
COPY . /boot
FROM rhel
# no EXPOSE instruction
FROM centos:7
EXPOSE 8000
EXPOSE 9090 9091
`,
want: []string{"8000", "9090", "9091"},
},
}
for name, tc := range testCases {
node, err := parser.Parse(strings.NewReader(tc.in))
if err != nil {
t.Errorf("%s: parse error: %v", name, err)
continue
}
got := LastExposedPorts(node)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("LastExposedPorts: %s: got %#v; want %#v", name, got, tc.want)
}
}
}
// TestLastExposedPortsNilNode tests calling LastExposedPorts with a nil
// *parser.Node.
func TestLastExposedPortsNilNode(t *testing.T) {
if got := LastExposedPorts(nil); got != nil {
t.Errorf("LastExposedPorts(nil) = %#v; want nil", got)
}
}
// TestExposedPorts tests calling exposedPorts with multiple valid combinations
// of input.
func TestExposedPorts(t *testing.T) {
testCases := map[string]struct {
in string
want [][]string
}{
"empty Dockerfile": {
in: ``,
want: nil,
},
"EXPOSE missing argument": {
in: `EXPOSE`,
want: nil,
},
"EXPOSE no FROM": {
in: `EXPOSE 8080`,
want: nil,
},
"single EXPOSE after FROM": {
in: `FROM centos:7
EXPOSE 8080`,
want: [][]string{{"8080"}},
},
"multiple EXPOSE and FROM": {
in: `# EXPOSE before FROM should be ignore
EXPOSE 777
FROM busybox
EXPOSE 8080
COPY . /boot
FROM rhel
# no EXPOSE instruction
FROM centos:7
EXPOSE 8000
EXPOSE 9090 9091
`,
want: [][]string{{"8080"}, nil, {"8000", "9090", "9091"}},
},
}
for name, tc := range testCases {
node, err := parser.Parse(strings.NewReader(tc.in))
if err != nil {
t.Errorf("%s: parse error: %v", name, err)
continue
}
got := exposedPorts(node)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("exposedPorts: %s: got %#v; want %#v", name, got, tc.want)
}
}
}
// TestExposedPortsNilNode tests calling exposedPorts with a nil *parser.Node.
func TestExposedPortsNilNode(t *testing.T) {
if got := exposedPorts(nil); got != nil {
t.Errorf("exposedPorts(nil) = %#v; want nil", got)
}
}
// TestNextValues tests calling nextValues with multiple valid combinations of
// input.
func TestNextValues(t *testing.T) {
testCases := map[string][]string{
`FROM busybox:latest`: {"busybox:latest"},
`MAINTAINER nobody@example.com`: {"nobody@example.com"},
`LABEL version=1.0`: {"version", "1.0"},
`EXPOSE 8080`: {"8080"},
`VOLUME /var/run/www`: {"/var/run/www"},
`ENV PATH=/bin`: {"PATH", "/bin"},
`ADD file /home/`: {"file", "/home/"},
`COPY dir/ /tmp/`: {"dir/", "/tmp/"},
`RUN echo "Hello world!"`: {`echo "Hello world!"`},
`ENTRYPOINT /bin/sh`: {"/bin/sh"},
`CMD ["-c", "env"]`: {"-c", "env"},
`USER 1001`: {"1001"},
`WORKDIR /home`: {"/home"},
}
for original, want := range testCases {
node, err := parser.Parse(strings.NewReader(original))
if err != nil {
t.Fatalf("parse error: %s: %v", original, err)
}
if len(node.Children) != 1 {
t.Fatalf("unexpected number of children in test case: %s", original)
}
// The Docker parser always wrap instructions in a root node.
// Look at the node representing the first instruction, the one
// and only one in each test case.
node = node.Children[0]
if got := nextValues(node); !reflect.DeepEqual(got, want) {
t.Errorf("nextValues(%+v) = %#v; want %#v", node, got, want)
}
}
}
// TestNextValuesOnbuild tests calling nextValues with ONBUILD instructions as
// input.
func TestNextValuesOnbuild(t *testing.T) {
testCases := map[string][]string{
`ONBUILD ADD . /app/src`: {".", "/app/src"},
`ONBUILD RUN echo "Hello universe!"`: {`echo "Hello universe!"`},
}
for original, want := range testCases {
node, err := parser.Parse(strings.NewReader(original))
if err != nil {
t.Fatalf("parse error: %s: %v", original, err)
}
if len(node.Children) != 1 {
t.Fatalf("unexpected number of children in test case: %s", original)
}
// The Docker parser always wrap instructions in a root node.
// Look at the node representing the instruction following
// ONBUILD, the one and only one in each test case.
node = node.Children[0].Next
if node == nil || len(node.Children) != 1 {
t.Fatalf("unexpected number of children in ONBUILD instruction of test case: %s", original)
}
node = node.Children[0]
if got := nextValues(node); !reflect.DeepEqual(got, want) {
t.Errorf("nextValues(%+v) = %#v; want %#v", node, got, want)
}
}
}
// TestNextValuesNilNode tests calling nextValues with a nil *parser.Node.
func TestNextValuesNilNode(t *testing.T) {
if got := nextValues(nil); got != nil {
t.Errorf("nextValues(nil) = %#v; want nil", got)
}
}