github.com/toplink-cn/moby@v0.0.0-20240305205811-460b4aebdf81/integration-cli/events_utils_test.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "io" 8 "os/exec" 9 "regexp" 10 "strconv" 11 "strings" 12 "testing" 13 14 "github.com/containerd/log" 15 eventstestutils "github.com/docker/docker/daemon/events/testutils" 16 "github.com/docker/docker/integration-cli/cli" 17 "gotest.tools/v3/assert" 18 ) 19 20 // eventMatcher is a function that tries to match an event input. 21 // It returns true if the event matches and a map with 22 // a set of key/value to identify the match. 23 type eventMatcher func(text string) (map[string]string, bool) 24 25 // eventMatchProcessor is a function to handle an event match. 26 // It receives a map of key/value with the information extracted in a match. 27 type eventMatchProcessor func(matches map[string]string) 28 29 // eventObserver runs an events commands and observes its output. 30 type eventObserver struct { 31 buffer *bytes.Buffer 32 command *exec.Cmd 33 scanner *bufio.Scanner 34 startTime string 35 disconnectionError error 36 } 37 38 // newEventObserver creates the observer and initializes the command 39 // without running it. Users must call `eventObserver.Start` to start the command. 40 func newEventObserver(c *testing.T, args ...string) (*eventObserver, error) { 41 since := daemonTime(c).Unix() 42 return newEventObserverWithBacklog(c, since, args...) 43 } 44 45 // newEventObserverWithBacklog creates a new observer changing the start time of the backlog to return. 46 func newEventObserverWithBacklog(c *testing.T, since int64, args ...string) (*eventObserver, error) { 47 startTime := strconv.FormatInt(since, 10) 48 cmdArgs := []string{"events", "--since", startTime} 49 if len(args) > 0 { 50 cmdArgs = append(cmdArgs, args...) 51 } 52 eventsCmd := exec.Command(dockerBinary, cmdArgs...) 53 stdout, err := eventsCmd.StdoutPipe() 54 if err != nil { 55 return nil, err 56 } 57 58 return &eventObserver{ 59 buffer: new(bytes.Buffer), 60 command: eventsCmd, 61 scanner: bufio.NewScanner(stdout), 62 startTime: startTime, 63 }, nil 64 } 65 66 // Start starts the events command. 67 func (e *eventObserver) Start() error { 68 return e.command.Start() 69 } 70 71 // Stop stops the events command. 72 func (e *eventObserver) Stop() { 73 e.command.Process.Kill() 74 e.command.Wait() 75 } 76 77 // Match tries to match the events output with a given matcher. 78 func (e *eventObserver) Match(match eventMatcher, process eventMatchProcessor) { 79 for e.scanner.Scan() { 80 text := e.scanner.Text() 81 e.buffer.WriteString(text) 82 e.buffer.WriteString("\n") 83 84 if matches, ok := match(text); ok { 85 process(matches) 86 } 87 } 88 89 err := e.scanner.Err() 90 if err == nil { 91 err = io.EOF 92 } 93 94 log.G(context.TODO()).Debugf("EventObserver scanner loop finished: %v", err) 95 e.disconnectionError = err 96 } 97 98 func (e *eventObserver) CheckEventError(c *testing.T, id, event string, match eventMatcher) { 99 var foundEvent bool 100 scannerOut := e.buffer.String() 101 102 if e.disconnectionError != nil { 103 until := daemonUnixTime(c) 104 out := cli.DockerCmd(c, "events", "--since", e.startTime, "--until", until).Stdout() 105 events := strings.Split(strings.TrimSpace(out), "\n") 106 for _, e := range events { 107 if _, ok := match(e); ok { 108 foundEvent = true 109 break 110 } 111 } 112 scannerOut = out 113 } 114 if !foundEvent { 115 c.Fatalf("failed to observe event `%s` for %s. Disconnection error: %v\nout:\n%v", event, id, e.disconnectionError, scannerOut) 116 } 117 } 118 119 // matchEventLine matches a text with the event regular expression. 120 // It returns the matches and true if the regular expression matches with the given id and event type. 121 // It returns an empty map and false if there is no match. 122 func matchEventLine(id, eventType string, actions map[string]chan bool) eventMatcher { 123 return func(text string) (map[string]string, bool) { 124 matches := eventstestutils.ScanMap(text) 125 if len(matches) == 0 { 126 return matches, false 127 } 128 129 if matchIDAndEventType(matches, id, eventType) { 130 if _, ok := actions[matches["action"]]; ok { 131 return matches, true 132 } 133 } 134 return matches, false 135 } 136 } 137 138 // processEventMatch closes an action channel when an event line matches the expected action. 139 func processEventMatch(actions map[string]chan bool) eventMatchProcessor { 140 return func(matches map[string]string) { 141 if ch, ok := actions[matches["action"]]; ok { 142 ch <- true 143 } 144 } 145 } 146 147 // parseEventAction parses an event text and returns the action. 148 // It fails if the text is not in the event format. 149 func parseEventAction(c *testing.T, text string) string { 150 matches := eventstestutils.ScanMap(text) 151 return matches["action"] 152 } 153 154 // eventActionsByIDAndType returns the actions for a given id and type. 155 // It fails if the text is not in the event format. 156 func eventActionsByIDAndType(c *testing.T, events []string, id, eventType string) []string { 157 var filtered []string 158 for _, event := range events { 159 matches := eventstestutils.ScanMap(event) 160 assert.Assert(c, matches != nil) 161 if matchIDAndEventType(matches, id, eventType) { 162 filtered = append(filtered, matches["action"]) 163 } 164 } 165 return filtered 166 } 167 168 // matchIDAndEventType returns true if an event matches a given id and type. 169 // It also resolves names in the event attributes if the id doesn't match. 170 func matchIDAndEventType(matches map[string]string, id, eventType string) bool { 171 return matchEventID(matches, id) && matches["eventType"] == eventType 172 } 173 174 func matchEventID(matches map[string]string, id string) bool { 175 matchID := matches["id"] == id || strings.HasPrefix(matches["id"], id) 176 if !matchID && matches["attributes"] != "" { 177 // try matching a name in the attributes 178 attributes := map[string]string{} 179 for _, a := range strings.Split(matches["attributes"], ", ") { 180 kv := strings.Split(a, "=") 181 attributes[kv[0]] = kv[1] 182 } 183 matchID = attributes["name"] == id 184 } 185 return matchID 186 } 187 188 func parseEvents(c *testing.T, out, match string) { 189 events := strings.Split(strings.TrimSpace(out), "\n") 190 for _, event := range events { 191 matches := eventstestutils.ScanMap(event) 192 matched, err := regexp.MatchString(match, matches["action"]) 193 assert.NilError(c, err) 194 assert.Assert(c, matched, "Matcher: %s did not match %s", match, matches["action"]) 195 } 196 }