integration-cli/events_utils_test.go
af51df20
 package main
 
 import (
 	"bufio"
 	"bytes"
 	"io"
 	"os/exec"
9d12d093
 	"regexp"
af51df20
 	"strconv"
72f1881d
 	"strings"
e25352a4
 	"testing"
af51df20
 
e03cc68e
 	eventstestutils "github.com/docker/docker/daemon/events/testutils"
1009e6a4
 	"github.com/sirupsen/logrus"
6345208b
 	"gotest.tools/assert"
af51df20
 )
 
 // eventMatcher is a function that tries to match an event input.
27b06049
 // It returns true if the event matches and a map with
 // a set of key/value to identify the match.
 type eventMatcher func(text string) (map[string]string, bool)
 
 // eventMatchProcessor is a function to handle an event match.
 // It receives a map of key/value with the information extracted in a match.
 type eventMatchProcessor func(matches map[string]string)
af51df20
 
 // eventObserver runs an events commands and observes its output.
 type eventObserver struct {
851fe00c
 	buffer             *bytes.Buffer
 	command            *exec.Cmd
 	scanner            *bufio.Scanner
 	startTime          string
 	disconnectionError error
af51df20
 }
 
 // newEventObserver creates the observer and initializes the command
 // without running it. Users must call `eventObserver.Start` to start the command.
64a928a3
 func newEventObserver(c *testing.T, args ...string) (*eventObserver, error) {
af51df20
 	since := daemonTime(c).Unix()
72f1881d
 	return newEventObserverWithBacklog(c, since, args...)
 }
af51df20
 
72f1881d
 // newEventObserverWithBacklog creates a new observer changing the start time of the backlog to return.
64a928a3
 func newEventObserverWithBacklog(c *testing.T, since int64, args ...string) (*eventObserver, error) {
851fe00c
 	startTime := strconv.FormatInt(since, 10)
 	cmdArgs := []string{"events", "--since", startTime}
af51df20
 	if len(args) > 0 {
 		cmdArgs = append(cmdArgs, args...)
 	}
 	eventsCmd := exec.Command(dockerBinary, cmdArgs...)
 	stdout, err := eventsCmd.StdoutPipe()
 	if err != nil {
 		return nil, err
 	}
 
 	return &eventObserver{
851fe00c
 		buffer:    new(bytes.Buffer),
 		command:   eventsCmd,
 		scanner:   bufio.NewScanner(stdout),
 		startTime: startTime,
af51df20
 	}, nil
 }
 
 // Start starts the events command.
 func (e *eventObserver) Start() error {
 	return e.command.Start()
 }
 
 // Stop stops the events command.
 func (e *eventObserver) Stop() {
 	e.command.Process.Kill()
ddae20c0
 	e.command.Wait()
af51df20
 }
 
 // Match tries to match the events output with a given matcher.
27b06049
 func (e *eventObserver) Match(match eventMatcher, process eventMatchProcessor) {
851fe00c
 	for e.scanner.Scan() {
 		text := e.scanner.Text()
af51df20
 		e.buffer.WriteString(text)
 		e.buffer.WriteString("\n")
 
27b06049
 		if matches, ok := match(text); ok {
 			process(matches)
 		}
af51df20
 	}
 
851fe00c
 	err := e.scanner.Err()
 	if err == nil {
 		err = io.EOF
 	}
 
d9177233
 	logrus.Debugf("EventObserver scanner loop finished: %v", err)
851fe00c
 	e.disconnectionError = err
af51df20
 }
 
64a928a3
 func (e *eventObserver) CheckEventError(c *testing.T, id, event string, match eventMatcher) {
851fe00c
 	var foundEvent bool
 	scannerOut := e.buffer.String()
 
 	if e.disconnectionError != nil {
55053d35
 		until := daemonUnixTime(c)
851fe00c
 		out, _ := dockerCmd(c, "events", "--since", e.startTime, "--until", until)
 		events := strings.Split(strings.TrimSpace(out), "\n")
 		for _, e := range events {
27b06049
 			if _, ok := match(e); ok {
851fe00c
 				foundEvent = true
 				break
 			}
 		}
 		scannerOut = out
 	}
 	if !foundEvent {
 		c.Fatalf("failed to observe event `%s` for %s. Disconnection error: %v\nout:\n%v", event, id, e.disconnectionError, scannerOut)
 	}
af51df20
 }
72f1881d
 
 // matchEventLine matches a text with the event regular expression.
27b06049
 // It returns the matches and true if the regular expression matches with the given id and event type.
 // It returns an empty map and false if there is no match.
72f1881d
 func matchEventLine(id, eventType string, actions map[string]chan bool) eventMatcher {
27b06049
 	return func(text string) (map[string]string, bool) {
a9f2006f
 		matches := eventstestutils.ScanMap(text)
851fe00c
 		if len(matches) == 0 {
27b06049
 			return matches, false
72f1881d
 		}
 
 		if matchIDAndEventType(matches, id, eventType) {
27b06049
 			if _, ok := actions[matches["action"]]; ok {
 				return matches, true
72f1881d
 			}
 		}
27b06049
 		return matches, false
 	}
 }
 
 // processEventMatch closes an action channel when an event line matches the expected action.
 func processEventMatch(actions map[string]chan bool) eventMatchProcessor {
 	return func(matches map[string]string) {
 		if ch, ok := actions[matches["action"]]; ok {
01095108
 			ch <- true
27b06049
 		}
72f1881d
 	}
 }
 
 // parseEventAction parses an event text and returns the action.
 // It fails if the text is not in the event format.
64a928a3
 func parseEventAction(c *testing.T, text string) string {
a9f2006f
 	matches := eventstestutils.ScanMap(text)
72f1881d
 	return matches["action"]
 }
 
 // eventActionsByIDAndType returns the actions for a given id and type.
 // It fails if the text is not in the event format.
64a928a3
 func eventActionsByIDAndType(c *testing.T, events []string, id, eventType string) []string {
72f1881d
 	var filtered []string
 	for _, event := range events {
a9f2006f
 		matches := eventstestutils.ScanMap(event)
6345208b
 		assert.Assert(c, matches != nil)
72f1881d
 		if matchIDAndEventType(matches, id, eventType) {
 			filtered = append(filtered, matches["action"])
 		}
 	}
 	return filtered
 }
 
 // matchIDAndEventType returns true if an event matches a given id and type.
 // It also resolves names in the event attributes if the id doesn't match.
 func matchIDAndEventType(matches map[string]string, id, eventType string) bool {
 	return matchEventID(matches, id) && matches["eventType"] == eventType
 }
 
 func matchEventID(matches map[string]string, id string) bool {
 	matchID := matches["id"] == id || strings.HasPrefix(matches["id"], id)
 	if !matchID && matches["attributes"] != "" {
 		// try matching a name in the attributes
 		attributes := map[string]string{}
 		for _, a := range strings.Split(matches["attributes"], ", ") {
 			kv := strings.Split(a, "=")
 			attributes[kv[0]] = kv[1]
 		}
 		matchID = attributes["name"] == id
 	}
 	return matchID
 }
 
64a928a3
 func parseEvents(c *testing.T, out, match string) {
72f1881d
 	events := strings.Split(strings.TrimSpace(out), "\n")
 	for _, event := range events {
a9f2006f
 		matches := eventstestutils.ScanMap(event)
72f1881d
 		matched, err := regexp.MatchString(match, matches["action"])
6345208b
 		assert.NilError(c, err)
 		assert.Assert(c, matched, "Matcher: %s did not match %s", match, matches["action"])
72f1881d
 	}
 }
 
64a928a3
 func parseEventsWithID(c *testing.T, out, match, id string) {
72f1881d
 	events := strings.Split(strings.TrimSpace(out), "\n")
 	for _, event := range events {
a9f2006f
 		matches := eventstestutils.ScanMap(event)
6345208b
 		assert.Assert(c, matchEventID(matches, id))
72f1881d
 
 		matched, err := regexp.MatchString(match, matches["action"])
6345208b
 		assert.NilError(c, err)
 		assert.Assert(c, matched, "Matcher: %s did not match %s", match, matches["action"])
72f1881d
 	}
 }