Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| // -*- Mode: Go; indent-tabs-mode: t -*- | |
| /* | |
| * Copyright (c) 2015 Canonical Ltd | |
| * | |
| * Licensed under either the Apache License, Version 2.0 or the BSD 3-clause | |
| * license at the users choice. A copy of both licenses are available in the | |
| * project source as Apache-2.0 and BSD. You may not use this file except in | |
| * compliance with one of these two licences. | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT | |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
| * license you chose for the specific language governing permissions and | |
| * limitations under that license. | |
| * | |
| */ | |
| // Package subunit provides a writer of the Subunit v2 protocol. | |
| package subunit | |
| import ( | |
| "bytes" | |
| "encoding/binary" | |
| "fmt" | |
| "hash/crc32" | |
| "io" | |
| "time" | |
| ) | |
| // ErrPacketLen represents an unhandled packet length error | |
| type ErrPacketLen struct { | |
| Length int | |
| } | |
| func (e *ErrPacketLen) Error() string { | |
| return fmt.Sprintf("packet too big (%d bytes)", e.Length) | |
| } | |
| // ErrNumber represents an error for a number that is over the size | |
| type ErrNumber struct { | |
| Number int | |
| } | |
| func (e *ErrNumber) Error() string { | |
| return fmt.Sprintf("number too big (%d)", e.Number) | |
| } | |
| const ( | |
| signature byte = 0xb3 | |
| // Flags high byte | |
| version byte = 0x2 | |
| testIDPresent byte = 0x8 | |
| timestampPresent byte = 0x2 | |
| // Flags low byte | |
| fileContentPresent byte = 0x40 | |
| mimePresent byte = 0x20 | |
| ) | |
| var status = map[string]byte{ | |
| "exists": 0x1, | |
| "inprogress": 0x2, | |
| "success": 0x3, | |
| "uxsuccess": 0x4, | |
| "skip": 0x5, | |
| "fail": 0x6, | |
| "xfail": 0x7, | |
| } | |
| func makeLen(baseLen int) (length int, err error) { | |
| length = baseLen + 4 // Add the length of the CRC32. | |
| // We need to take into account the variable length of the length field itself. | |
| switch { | |
| case length <= 62: | |
| // Fits in one byte. | |
| length++ | |
| case length <= 16381: | |
| // Fits in two bytes. | |
| length += 2 | |
| case length <= 4194300: | |
| // Fits in three bytes. | |
| length += 3 | |
| default: | |
| err = &ErrPacketLen{length} | |
| } | |
| return length, err | |
| } | |
| // StreamResultToBytes is an implementation of the StreamResult API that converts calls to bytes. | |
| type StreamResultToBytes struct { | |
| Output io.Writer | |
| } | |
| // Event is a status or a file attachment event. | |
| type Event struct { | |
| TestID string | |
| Status string | |
| Timestamp time.Time | |
| FileName string | |
| FileBytes []byte | |
| MIME string | |
| } | |
| type packetPart struct { | |
| bytes []byte | |
| err error | |
| } | |
| func (e *Event) write(writer io.Writer) error { | |
| // PACKET := SIGNATURE FLAGS PACKET_LENGTH TIMESTAMP? TESTID? TAGS? MIME? FILECONTENT? | |
| // ROUTING_CODE? CRC32 | |
| flagsChan := make(chan []byte) | |
| go e.makeFlags(flagsChan) | |
| timestampChan := make(chan packetPart) | |
| go e.makeTimestamp(timestampChan) | |
| idChan := make(chan packetPart) | |
| go e.makeTestID(idChan) | |
| mimeChan := make(chan packetPart) | |
| go e.makeMIME(mimeChan) | |
| fileContentChan := make(chan packetPart) | |
| go e.makeFileContent(fileContentChan) | |
| // We construct a temporary buffer because we won't know the length until it's finished. | |
| // Then we insert the length. | |
| var bTemp bytes.Buffer | |
| bTemp.WriteByte(signature) | |
| bTemp.Write(<-flagsChan) | |
| for _, part := range []packetPart{<-timestampChan, <-idChan, <-mimeChan, <-fileContentChan} { | |
| if part.err != nil { | |
| return part.err | |
| } | |
| bTemp.Write(part.bytes) | |
| } | |
| length, err := makeLen(bTemp.Len()) | |
| if err != nil { | |
| return err | |
| } | |
| // Insert the length. | |
| var b bytes.Buffer | |
| b.Write(bTemp.Next(3)) // signature (1 byte) and flags (2 bytes) | |
| writeNumber(&b, length) | |
| b.Write(bTemp.Next(bTemp.Len())) | |
| // Add the CRC32 | |
| crc := crc32.ChecksumIEEE(b.Bytes()) | |
| binary.Write(&b, binary.BigEndian, crc) | |
| _, err = writer.Write(b.Bytes()) | |
| return err | |
| } | |
| func (e *Event) makeFlags(c chan<- []byte) { | |
| flags := make([]byte, 2, 2) | |
| flags[0] = version << 4 | |
| if e.TestID != "" { | |
| flags[0] = flags[0] | testIDPresent | |
| } | |
| if !e.Timestamp.IsZero() { | |
| flags[0] = flags[0] | timestampPresent | |
| } | |
| if e.FileName != "" { | |
| flags[1] = flags[1] | fileContentPresent | |
| } | |
| if e.MIME != "" { | |
| flags[1] = flags[1] | mimePresent | |
| } | |
| flags[1] = flags[1] | status[e.Status] | |
| c <- flags | |
| } | |
| func (e *Event) makeTestID(c chan<- packetPart) { | |
| var testID bytes.Buffer | |
| var err error | |
| if e.TestID != "" { | |
| err = writeUTF8(&testID, e.TestID) | |
| } | |
| c <- packetPart{testID.Bytes(), err} | |
| } | |
| func (e *Event) makeTimestamp(c chan<- packetPart) { | |
| var timestamp bytes.Buffer | |
| var err error | |
| if !e.Timestamp.IsZero() { | |
| err = binary.Write(×tamp, binary.BigEndian, uint32(e.Timestamp.Unix())) | |
| if err == nil { | |
| err = writeNumber(×tamp, int(e.Timestamp.UnixNano()%1000000000)) | |
| } | |
| } | |
| c <- packetPart{timestamp.Bytes(), err} | |
| } | |
| func (e *Event) makeMIME(c chan<- packetPart) { | |
| var mime bytes.Buffer | |
| var err error | |
| if e.MIME != "" { | |
| err = writeUTF8(&mime, e.MIME) | |
| } | |
| c <- packetPart{mime.Bytes(), err} | |
| } | |
| func (e *Event) makeFileContent(c chan<- packetPart) { | |
| var fileContent bytes.Buffer | |
| var err error | |
| if e.FileName != "" { | |
| err = writeUTF8(&fileContent, e.FileName) | |
| if err == nil { | |
| err = writeNumber(&fileContent, len(e.FileBytes)) | |
| } | |
| if err == nil { | |
| _, err = fileContent.Write(e.FileBytes) | |
| } | |
| } | |
| c <- packetPart{fileContent.Bytes(), err} | |
| } | |
| func writeUTF8(b io.Writer, s string) error { | |
| err := writeNumber(b, len(s)) | |
| if err != nil { | |
| return err | |
| } | |
| _, err = io.WriteString(b, s) | |
| return err | |
| } | |
| func writeNumber(b io.Writer, num int) (err error) { | |
| // The first two bits encode the size: | |
| // 00 = 1 byte | |
| // 01 = 2 bytes | |
| // 10 = 3 bytes | |
| // 11 = 4 bytes | |
| switch { | |
| case num < 64: // 2^(8-2) | |
| // Fits in one byte. | |
| binary.Write(b, binary.BigEndian, uint8(num)) | |
| case num < 16384: // 2^(16-2) | |
| // Fits in two bytes. | |
| binary.Write(b, binary.BigEndian, uint16(num|0x4000)) // Set the size to 01. | |
| case num < 4194304: // 2^(24-2) | |
| // Fits in three bytes. | |
| // Drop the two least significant bytes and set the size to 10. | |
| binary.Write(b, binary.BigEndian, uint8((num>>16)|0x80)) | |
| // Drop the two most significant bytes. | |
| binary.Write(b, binary.BigEndian, uint16(num&0xffff)) | |
| case num < 1073741824: // 2^(32-2): | |
| // Fits in four bytes. | |
| // Set the size to 11. | |
| binary.Write(b, binary.BigEndian, uint32(num|0xc0000000)) | |
| default: | |
| err = &ErrNumber{num} | |
| } | |
| return err | |
| } | |
| // Status informs the result about a test status. | |
| func (s *StreamResultToBytes) Status(e Event) error { | |
| return e.write(s.Output) | |
| } |