Source file src/cmd/internal/test2json/test2json.go

     1  // Copyright 2017 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package test2json implements conversion of test binary output to JSON.
     6  // It is used by cmd/test2json and cmd/go.
     7  //
     8  // See the cmd/test2json documentation for details of the JSON encoding.
     9  package test2json
    10  
    11  import (
    12  	"bytes"
    13  	"encoding/json"
    14  	"fmt"
    15  	"io"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  	"unicode"
    20  	"unicode/utf8"
    21  )
    22  
    23  // Mode controls details of the conversion.
    24  type Mode int
    25  
    26  const (
    27  	Timestamp Mode = 1 << iota // include Time in events
    28  )
    29  
    30  // event is the JSON struct we emit.
    31  type event struct {
    32  	Time        *time.Time `json:",omitempty"`
    33  	Action      string
    34  	Package     string     `json:",omitempty"`
    35  	Test        string     `json:",omitempty"`
    36  	Elapsed     *float64   `json:",omitempty"`
    37  	Output      *textBytes `json:",omitempty"`
    38  	OutputType  string     `json:",omitempty"`
    39  	FailedBuild string     `json:",omitempty"`
    40  	Key         string     `json:",omitempty"`
    41  	Value       string     `json:",omitempty"`
    42  	Path        string     `json:",omitempty"`
    43  }
    44  
    45  // textBytes is a hack to get JSON to emit a []byte as a string
    46  // without actually copying it to a string.
    47  // It implements encoding.TextMarshaler, which returns its text form as a []byte,
    48  // and then json encodes that text form as a string (which was our goal).
    49  type textBytes []byte
    50  
    51  func (b textBytes) MarshalText() ([]byte, error) { return b, nil }
    52  
    53  // A Converter holds the state of a test-to-JSON conversion.
    54  // It implements io.WriteCloser; the caller writes test output in,
    55  // and the converter writes JSON output to w.
    56  type Converter struct {
    57  	w           io.Writer  // JSON output stream
    58  	pkg         string     // package to name in events
    59  	mode        Mode       // mode bits
    60  	start       time.Time  // time converter started
    61  	testName    string     // name of current test, for output attribution
    62  	report      []*event   // pending test result reports (nested for subtests)
    63  	result      string     // overall test result if seen
    64  	input       lineBuffer // input buffer
    65  	output      lineBuffer // output buffer
    66  	markFraming bool       // require ^V marker to introduce test framing line
    67  	markErrEnd  bool       // within an error, require ^N marker to end
    68  	markEscape  bool       // the next character should be considered to be escaped
    69  	isFraming   bool       // indicates the output being written is framing
    70  
    71  	// failedBuild is set to the package ID of the cause of a build failure,
    72  	// if that's what caused this test to fail.
    73  	failedBuild string
    74  }
    75  
    76  // inBuffer and outBuffer are the input and output buffer sizes.
    77  // They're variables so that they can be reduced during testing.
    78  //
    79  // The input buffer needs to be able to hold any single test
    80  // directive line we want to recognize, like:
    81  //
    82  //	<many spaces> --- PASS: very/nested/s/u/b/t/e/s/t
    83  //
    84  // If anyone reports a test directive line > 4k not working, it will
    85  // be defensible to suggest they restructure their test or test names.
    86  //
    87  // The output buffer must be >= utf8.UTFMax, so that it can
    88  // accumulate any single UTF8 sequence. Lines that fit entirely
    89  // within the output buffer are emitted in single output events.
    90  // Otherwise they are split into multiple events.
    91  // The output buffer size therefore limits the size of the encoding
    92  // of a single JSON output event. 1k seems like a reasonable balance
    93  // between wanting to avoid splitting an output line and not wanting to
    94  // generate enormous output events.
    95  var (
    96  	inBuffer  = 4096
    97  	outBuffer = 1024
    98  )
    99  
   100  // NewConverter returns a "test to json" converter.
   101  // Writes on the returned writer are written as JSON to w,
   102  // with minimal delay.
   103  //
   104  // Writes on the returned writer are expected to contain markers. Test framing
   105  // such as "=== RUN" and friends are expected to be prefixed with ^V (\x22).
   106  // Error output is expected to be prefixed with ^O (\x0f) and suffixed with ^N
   107  // (\x0e). Other occurrences of these control characters (e.g. calls to T.Log)
   108  // must be escaped with ^[ (\x1b). Test framing will generate events such as
   109  // start, run, etc as well as output events with an output type of "frame".
   110  // Error output will generate output events with an output type of "error" or
   111  // "error-continue". See cmd/test2json help for details.
   112  //
   113  // The writes to w are whole JSON events ending in \n,
   114  // so that it is safe to run multiple tests writing to multiple converters
   115  // writing to a single underlying output stream w.
   116  // As long as the underlying output w can handle concurrent writes
   117  // from multiple goroutines, the result will be a JSON stream
   118  // describing the relative ordering of execution in all the concurrent tests.
   119  //
   120  // The mode flag adjusts the behavior of the converter.
   121  // Passing ModeTime includes event timestamps and elapsed times.
   122  //
   123  // The pkg string, if present, specifies the import path to
   124  // report in the JSON stream.
   125  func NewConverter(w io.Writer, pkg string, mode Mode) *Converter {
   126  	c := new(Converter)
   127  	*c = Converter{
   128  		w:     w,
   129  		pkg:   pkg,
   130  		mode:  mode,
   131  		start: time.Now(),
   132  		input: lineBuffer{
   133  			b:    make([]byte, 0, inBuffer),
   134  			line: c.handleInputLine,
   135  			part: c.output.write,
   136  		},
   137  		output: lineBuffer{
   138  			b:    make([]byte, 0, outBuffer),
   139  			line: c.writeOutputEvent,
   140  			part: c.writeOutputEvent,
   141  		},
   142  	}
   143  	c.writeEvent(&event{Action: "start"})
   144  	return c
   145  }
   146  
   147  // Write writes the test input to the converter.
   148  func (c *Converter) Write(b []byte) (int, error) {
   149  	c.input.write(b)
   150  	return len(b), nil
   151  }
   152  
   153  // Exited marks the test process as having exited with the given error.
   154  func (c *Converter) Exited(err error) {
   155  	if err == nil {
   156  		if c.result != "skip" {
   157  			c.result = "pass"
   158  		}
   159  	} else {
   160  		c.result = "fail"
   161  	}
   162  }
   163  
   164  // SetFailedBuild sets the package ID that is the root cause of a build failure
   165  // for this test. This will be reported in the final "fail" event's FailedBuild
   166  // field.
   167  func (c *Converter) SetFailedBuild(pkgID string) {
   168  	c.failedBuild = pkgID
   169  }
   170  
   171  const (
   172  	markFraming  byte = 'V' &^ '@' // ^V: framing
   173  	markErrBegin byte = 'O' &^ '@' // ^O: start of error
   174  	markErrEnd   byte = 'N' &^ '@' // ^N: end of error
   175  	markEscape   byte = '[' &^ '@' // ^[: escape
   176  )
   177  
   178  var (
   179  	// printed by test on successful run.
   180  	bigPass = []byte("PASS")
   181  
   182  	// printed by test after a normal test failure.
   183  	bigFail = []byte("FAIL")
   184  
   185  	// printed by 'go test' along with an error if the test binary terminates
   186  	// with an error.
   187  	bigFailErrorPrefix = []byte("FAIL\t")
   188  
   189  	// an === NAME line with no test name, if trailing spaces are deleted
   190  	emptyName     = []byte("=== NAME")
   191  	emptyNameLine = []byte("=== NAME  \n")
   192  
   193  	updates = [][]byte{
   194  		[]byte("=== RUN   "),
   195  		[]byte("=== PAUSE "),
   196  		[]byte("=== CONT  "),
   197  		[]byte("=== NAME  "),
   198  		[]byte("=== PASS  "),
   199  		[]byte("=== FAIL  "),
   200  		[]byte("=== SKIP  "),
   201  		[]byte("=== ATTR  "),
   202  		[]byte("=== ARTIFACTS "),
   203  	}
   204  
   205  	reports = [][]byte{
   206  		[]byte("--- PASS: "),
   207  		[]byte("--- FAIL: "),
   208  		[]byte("--- SKIP: "),
   209  		[]byte("--- BENCH: "),
   210  	}
   211  
   212  	fourSpace = []byte("    ")
   213  
   214  	skipLinePrefix = []byte("?   \t")
   215  	skipLineSuffix = []byte("\t[no test files]")
   216  )
   217  
   218  // handleInputLine handles a single whole test output line.
   219  // It must write the line to c.output but may choose to do so
   220  // before or after emitting other events.
   221  func (c *Converter) handleInputLine(line []byte) {
   222  	if len(line) == 0 {
   223  		return
   224  	}
   225  	sawMarker := false
   226  	if c.markFraming && line[0] != markFraming {
   227  		c.output.write(line)
   228  		return
   229  	}
   230  	if line[0] == markFraming {
   231  		c.output.flush()
   232  		sawMarker = true
   233  		line = line[1:]
   234  	}
   235  
   236  	// Trim is line without \n or \r\n.
   237  	trim := line
   238  	if len(trim) > 0 && trim[len(trim)-1] == '\n' {
   239  		trim = trim[:len(trim)-1]
   240  		if len(trim) > 0 && trim[len(trim)-1] == '\r' {
   241  			trim = trim[:len(trim)-1]
   242  		}
   243  	}
   244  
   245  	// === CONT followed by an empty test name can lose its trailing spaces.
   246  	if bytes.Equal(trim, emptyName) {
   247  		line = emptyNameLine
   248  		trim = line[:len(line)-1]
   249  	}
   250  
   251  	// Final PASS or FAIL.
   252  	if bytes.Equal(trim, bigPass) || bytes.Equal(trim, bigFail) || bytes.HasPrefix(trim, bigFailErrorPrefix) {
   253  		c.flushReport(0)
   254  		c.testName = ""
   255  		c.markFraming = sawMarker
   256  		c.writeFraming(line)
   257  		if bytes.Equal(trim, bigPass) {
   258  			c.result = "pass"
   259  		} else {
   260  			c.result = "fail"
   261  		}
   262  		return
   263  	}
   264  
   265  	// Special case for entirely skipped test binary: "?   \tpkgname\t[no test files]\n" is only line.
   266  	// Report it as plain output but remember to say skip in the final summary.
   267  	if bytes.HasPrefix(line, skipLinePrefix) && bytes.HasSuffix(trim, skipLineSuffix) && len(c.report) == 0 {
   268  		c.result = "skip"
   269  	}
   270  
   271  	// "=== RUN   "
   272  	// "=== PAUSE "
   273  	// "=== CONT  "
   274  	origLine := line
   275  	ok := false
   276  	indent := 0
   277  	for _, magic := range updates {
   278  		if bytes.HasPrefix(line, magic) {
   279  			ok = true
   280  			break
   281  		}
   282  	}
   283  	if !ok {
   284  		// "--- PASS: "
   285  		// "--- FAIL: "
   286  		// "--- SKIP: "
   287  		// "--- BENCH: "
   288  		// but possibly indented.
   289  		for bytes.HasPrefix(line, fourSpace) {
   290  			line = line[4:]
   291  			indent++
   292  		}
   293  		for _, magic := range reports {
   294  			if bytes.HasPrefix(line, magic) {
   295  				ok = true
   296  				break
   297  			}
   298  		}
   299  	}
   300  
   301  	// Not a special test output line.
   302  	if !ok {
   303  		// Lookup the name of the test which produced the output using the
   304  		// indentation of the output as an index into the stack of the current
   305  		// subtests.
   306  		// If the indentation is greater than the number of current subtests
   307  		// then the output must have included extra indentation. We can't
   308  		// determine which subtest produced this output, so we default to the
   309  		// old behaviour of assuming the most recently run subtest produced it.
   310  		if indent > 0 && indent <= len(c.report) {
   311  			c.testName = c.report[indent-1].Test
   312  		}
   313  		c.output.write(origLine)
   314  		return
   315  	}
   316  
   317  	// Parse out action and test name from "=== ACTION: Name".
   318  	action, name, _ := strings.Cut(string(line[len("=== "):]), " ")
   319  	action = strings.TrimSuffix(action, ":")
   320  	action = strings.ToLower(action)
   321  	name = strings.TrimSpace(name)
   322  
   323  	e := &event{Action: action}
   324  	if line[0] == '-' { // PASS or FAIL report
   325  		// Parse out elapsed time.
   326  		if i := strings.Index(name, " ("); i >= 0 {
   327  			if strings.HasSuffix(name, "s)") {
   328  				t, err := strconv.ParseFloat(name[i+2:len(name)-2], 64)
   329  				if err == nil {
   330  					if c.mode&Timestamp != 0 {
   331  						e.Elapsed = &t
   332  					}
   333  				}
   334  			}
   335  			name = name[:i]
   336  		}
   337  		if len(c.report) < indent {
   338  			// Nested deeper than expected.
   339  			// Treat this line as plain output.
   340  			c.output.write(origLine)
   341  			return
   342  		}
   343  		// Flush reports at this indentation level or deeper.
   344  		c.markFraming = sawMarker
   345  		c.flushReport(indent)
   346  		e.Test = name
   347  		c.testName = name
   348  		c.report = append(c.report, e)
   349  		c.writeFraming(origLine)
   350  		return
   351  	}
   352  	switch action {
   353  	case "artifacts":
   354  		name, e.Path, _ = strings.Cut(name, " ")
   355  	case "attr":
   356  		var rest string
   357  		name, rest, _ = strings.Cut(name, " ")
   358  		e.Key, e.Value, _ = strings.Cut(rest, " ")
   359  	}
   360  	// === update.
   361  	// Finish any pending PASS/FAIL reports.
   362  	c.markFraming = sawMarker
   363  	c.flushReport(0)
   364  	c.testName = name
   365  
   366  	if action == "name" {
   367  		// This line is only generated to get c.testName right.
   368  		// Don't emit an event.
   369  		return
   370  	}
   371  
   372  	if action == "pause" {
   373  		// For a pause, we want to write the pause notification before
   374  		// delivering the pause event, just so it doesn't look like the test
   375  		// is generating output immediately after being paused.
   376  		c.writeFraming(origLine)
   377  	}
   378  	c.writeEvent(e)
   379  	if action != "pause" {
   380  		c.writeFraming(origLine)
   381  	}
   382  
   383  	return
   384  }
   385  
   386  func (c *Converter) writeFraming(line []byte) {
   387  	// This is a less than ideal way to 'pass' state around, but it's the best
   388  	// we can do without substantially modifying the line buffer.
   389  	c.isFraming = true
   390  	defer func() { c.isFraming = false }()
   391  	c.output.write(line)
   392  }
   393  
   394  // flushReport flushes all pending PASS/FAIL reports at levels >= depth.
   395  func (c *Converter) flushReport(depth int) {
   396  	c.testName = ""
   397  	for len(c.report) > depth {
   398  		e := c.report[len(c.report)-1]
   399  		c.report = c.report[:len(c.report)-1]
   400  		c.writeEvent(e)
   401  	}
   402  }
   403  
   404  // Close marks the end of the go test output.
   405  // It flushes any pending input and then output (only partial lines at this point)
   406  // and then emits the final overall package-level pass/fail event.
   407  func (c *Converter) Close() error {
   408  	c.input.flush()
   409  	c.output.flush()
   410  	if c.result != "" {
   411  		e := &event{Action: c.result}
   412  		if c.mode&Timestamp != 0 {
   413  			dt := time.Since(c.start).Round(1 * time.Millisecond).Seconds()
   414  			e.Elapsed = &dt
   415  		}
   416  		if c.result == "fail" {
   417  			e.FailedBuild = c.failedBuild
   418  		}
   419  		c.writeEvent(e)
   420  	}
   421  	return nil
   422  }
   423  
   424  // writeOutputEvent writes a single output event with the given bytes.
   425  func (c *Converter) writeOutputEvent(out []byte) {
   426  	var typ string
   427  	if c.isFraming {
   428  		typ = "frame"
   429  	} else if c.markErrEnd {
   430  		typ = "error-continue"
   431  	}
   432  
   433  	// Check for markers.
   434  	//
   435  	// An escape mark and the character it escapes may be passed in separate
   436  	// buffers. We must maintain state between calls to account for this, thus
   437  	// [Converter.markEscape] is set on one loop iteration and used to skip a
   438  	// character on the next.
   439  	//
   440  	// In most cases, [markErrBegin] will be the first character of a line and
   441  	// [markErrEnd] will be the last. However we cannot rely on that. For
   442  	// example, if a call to [T.Error] is preceded by a call to [fmt.Print] that
   443  	// does not print a newline. Thus we track the error status with
   444  	// [Converter.markErrEnd] and issue separate events if there is content
   445  	// before [markErrBegin] or after [markErrEnd].
   446  	for i := 0; i < len(out); i++ {
   447  		if c.markEscape {
   448  			c.markEscape = false
   449  			continue
   450  		}
   451  
   452  		switch out[i] {
   453  		case markEscape:
   454  			// Elide the mark
   455  			out = append(out[:i], out[i+1:]...)
   456  			i--
   457  
   458  			// Skip the next character
   459  			c.markEscape = true
   460  
   461  		case markErrBegin:
   462  			// If there is content before the mark, emit it as a separate event
   463  			if i > 0 {
   464  				out2 := out[:i]
   465  				c.writeEvent(&event{
   466  					Action:     "output",
   467  					Output:     (*textBytes)(&out2),
   468  					OutputType: typ,
   469  				})
   470  			}
   471  
   472  			// Process the error
   473  			c.markErrEnd = true
   474  			typ = "error"
   475  			out = out[i+1:]
   476  			i = 0
   477  
   478  		case markErrEnd:
   479  			// Elide the mark
   480  			out = append(out[:i], out[i+1:]...)
   481  
   482  			// If the next character is \n, include it
   483  			if i < len(out) && out[i] == '\n' {
   484  				i++
   485  			}
   486  
   487  			// Emit the error
   488  			out2 := out[:i]
   489  			c.writeEvent(&event{
   490  				Action:     "output",
   491  				Output:     (*textBytes)(&out2),
   492  				OutputType: typ,
   493  			})
   494  
   495  			// Process the rest
   496  			c.markErrEnd = false
   497  			typ = ""
   498  			out = out[i:]
   499  			i = 0
   500  		}
   501  	}
   502  
   503  	// Send the remaining output
   504  	if len(out) > 0 {
   505  		c.writeEvent(&event{
   506  			Action:     "output",
   507  			Output:     (*textBytes)(&out),
   508  			OutputType: typ,
   509  		})
   510  	}
   511  }
   512  
   513  // writeEvent writes a single event.
   514  // It adds the package, time (if requested), and test name (if needed).
   515  func (c *Converter) writeEvent(e *event) {
   516  	e.Package = c.pkg
   517  	if c.mode&Timestamp != 0 {
   518  		t := time.Now()
   519  		e.Time = &t
   520  	}
   521  	if e.Test == "" {
   522  		e.Test = c.testName
   523  	}
   524  	js, err := json.Marshal(e)
   525  	if err != nil {
   526  		// Should not happen - event is valid for json.Marshal.
   527  		fmt.Fprintf(c.w, "testjson internal error: %v\n", err)
   528  		return
   529  	}
   530  	js = append(js, '\n')
   531  	c.w.Write(js)
   532  }
   533  
   534  // A lineBuffer is an I/O buffer that reacts to writes by invoking
   535  // input-processing callbacks on whole lines or (for long lines that
   536  // have been split) line fragments.
   537  //
   538  // It should be initialized with b set to a buffer of length 0 but non-zero capacity,
   539  // and line and part set to the desired input processors.
   540  // The lineBuffer will call line(x) for any whole line x (including the final newline)
   541  // that fits entirely in cap(b). It will handle input lines longer than cap(b) by
   542  // calling part(x) for sections of the line. The line will be split at UTF8 boundaries,
   543  // and the final call to part for a long line includes the final newline.
   544  type lineBuffer struct {
   545  	b       []byte       // buffer
   546  	mid     bool         // whether we're in the middle of a long line
   547  	line    func([]byte) // line callback
   548  	part    func([]byte) // partial line callback
   549  	escaped bool
   550  }
   551  
   552  // write writes b to the buffer.
   553  func (l *lineBuffer) write(b []byte) {
   554  	for len(b) > 0 {
   555  		// Copy what we can into l.b.
   556  		m := copy(l.b[len(l.b):cap(l.b)], b)
   557  		l.b = l.b[:len(l.b)+m]
   558  		b = b[m:]
   559  
   560  		// Process lines in l.b.
   561  		i := 0
   562  		for i < len(l.b) {
   563  			j, w := l.indexEOL(l.b[i:])
   564  			if j < 0 {
   565  				if !l.mid {
   566  					if j := bytes.IndexByte(l.b[i:], '\t'); j >= 0 {
   567  						if isBenchmarkName(bytes.TrimRight(l.b[i:i+j], " ")) {
   568  							l.part(l.b[i : i+j+1])
   569  							l.mid = true
   570  							i += j + 1
   571  						}
   572  					}
   573  				}
   574  				break
   575  			}
   576  			e := i + j + w
   577  			if l.mid {
   578  				// Found the end of a partial line.
   579  				l.part(l.b[i:e])
   580  				l.mid = false
   581  			} else {
   582  				// Found a whole line.
   583  				l.line(l.b[i:e])
   584  			}
   585  			i = e
   586  		}
   587  
   588  		// Whatever's left in l.b is a line fragment.
   589  		if i == 0 && len(l.b) == cap(l.b) {
   590  			// The whole buffer is a fragment.
   591  			// Emit it as the beginning (or continuation) of a partial line.
   592  			t := trimUTF8(l.b)
   593  			l.part(l.b[:t])
   594  			l.b = l.b[:copy(l.b, l.b[t:])]
   595  			l.mid = true
   596  		}
   597  
   598  		// There's room for more input.
   599  		// Slide it down in hope of completing the line.
   600  		if i > 0 {
   601  			l.b = l.b[:copy(l.b, l.b[i:])]
   602  		}
   603  	}
   604  }
   605  
   606  // indexEOL finds the index of a line ending,
   607  // returning its position and output width.
   608  // A line ending is either a \n or the empty string just before a ^V not beginning a line.
   609  // The output width for \n is 1 (meaning it should be printed)
   610  // but the output width for ^V is 0 (meaning it should be left to begin the next line).
   611  func (l *lineBuffer) indexEOL(b []byte) (pos, wid int) {
   612  	for i, c := range b {
   613  		// Escape has no effect on \n
   614  		if c == '\n' {
   615  			return i, 1
   616  		}
   617  
   618  		// Ignore this character if the previous one was ^[
   619  		if l.escaped {
   620  			l.escaped = false
   621  			continue
   622  		}
   623  
   624  		// If this character is `^[`, set the escaped flag and continue
   625  		if c == markEscape {
   626  			l.escaped = true
   627  			continue
   628  		}
   629  
   630  		if c == markFraming && i > 0 { // test -v=json emits ^V at start of framing lines
   631  			return i, 0
   632  		}
   633  	}
   634  	return -1, 0
   635  }
   636  
   637  // flush flushes the line buffer.
   638  func (l *lineBuffer) flush() {
   639  	if len(l.b) > 0 {
   640  		// Must be a line without a \n, so a partial line.
   641  		l.part(l.b)
   642  		l.b = l.b[:0]
   643  	}
   644  }
   645  
   646  var benchmark = []byte("Benchmark")
   647  
   648  // isBenchmarkName reports whether b is a valid benchmark name
   649  // that might appear as the first field in a benchmark result line.
   650  func isBenchmarkName(b []byte) bool {
   651  	if !bytes.HasPrefix(b, benchmark) {
   652  		return false
   653  	}
   654  	if len(b) == len(benchmark) { // just "Benchmark"
   655  		return true
   656  	}
   657  	r, _ := utf8.DecodeRune(b[len(benchmark):])
   658  	return !unicode.IsLower(r)
   659  }
   660  
   661  // trimUTF8 returns a length t as close to len(b) as possible such that b[:t]
   662  // does not end in the middle of a possibly-valid UTF-8 sequence.
   663  //
   664  // If a large text buffer must be split before position i at the latest,
   665  // splitting at position trimUTF(b[:i]) avoids splitting a UTF-8 sequence.
   666  func trimUTF8(b []byte) int {
   667  	// Scan backward to find non-continuation byte.
   668  	for i := 1; i < utf8.UTFMax && i <= len(b); i++ {
   669  		if c := b[len(b)-i]; c&0xc0 != 0x80 {
   670  			switch {
   671  			case c&0xe0 == 0xc0:
   672  				if i < 2 {
   673  					return len(b) - i
   674  				}
   675  			case c&0xf0 == 0xe0:
   676  				if i < 3 {
   677  					return len(b) - i
   678  				}
   679  			case c&0xf8 == 0xf0:
   680  				if i < 4 {
   681  					return len(b) - i
   682  				}
   683  			}
   684  			break
   685  		}
   686  	}
   687  	return len(b)
   688  }
   689  

View as plain text