mirror of
				https://github.com/golang/go.git
				synced 2025-10-31 08:40:55 +00:00 
			
		
		
		
	cmd/internal/obj/x86, cmd/internal/ld, cmd/6l: 6g/asm -dynlink accesses global data via a GOT
Change-Id: I49862e177045369d6c94d6a58afbdace4f13cc96 Reviewed-on: https://go-review.googlesource.com/8237 Reviewed-by: Ian Lance Taylor <iant@golang.org>
This commit is contained in:
		
							parent
							
								
									969f10140c
								
							
						
					
					
						commit
						84207a2500
					
				
					 13 changed files with 292 additions and 7 deletions
				
			
		|  | @ -61,6 +61,9 @@ func betypeinit() { | |||
| 		typedefs[2].Sameas = gc.TUINT32 | ||||
| 	} | ||||
| 
 | ||||
| 	if gc.Ctxt.Flag_dynlink { | ||||
| 		gc.Thearch.ReservedRegs = append(gc.Thearch.ReservedRegs, x86.REG_R15) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
|  |  | |||
|  | @ -123,7 +123,9 @@ func BtoR(b uint64) int { | |||
| 		// BP is part of the calling convention if framepointer_enabled. | ||||
| 		b &^= (1 << (x86.REG_BP - x86.REG_AX)) | ||||
| 	} | ||||
| 
 | ||||
| 	if gc.Ctxt.Flag_dynlink { | ||||
| 		b &^= (1 << (x86.REG_R15 - x86.REG_AX)) | ||||
| 	} | ||||
| 	if b == 0 { | ||||
| 		return 0 | ||||
| 	} | ||||
|  |  | |||
|  | @ -332,6 +332,13 @@ func elfreloc1(r *ld.Reloc, sectoff int64) int { | |||
| 			return -1 | ||||
| 		} | ||||
| 
 | ||||
| 	case ld.R_GOTPCREL: | ||||
| 		if r.Siz == 4 { | ||||
| 			ld.Thearch.Vput(ld.R_X86_64_GOTPCREL | uint64(elfsym)<<32) | ||||
| 		} else { | ||||
| 			return -1 | ||||
| 		} | ||||
| 
 | ||||
| 	case ld.R_TLS: | ||||
| 		if r.Siz == 4 { | ||||
| 			if ld.Buildmode == ld.BuildmodeCShared { | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ var ( | |||
| 	PrintOut   = flag.Bool("S", false, "print assembly and machine code") | ||||
| 	TrimPath   = flag.String("trimpath", "", "remove prefix from recorded source file paths") | ||||
| 	Shared     = flag.Bool("shared", false, "generate code that can be linked into a shared library") | ||||
| 	Dynlink    = flag.Bool("dynlink", false, "support references to Go symbols defined in other shared libraries") | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
|  |  | |||
|  | @ -41,7 +41,8 @@ func main() { | |||
| 		ctxt.Debugasm = 1 | ||||
| 	} | ||||
| 	ctxt.Trimpath = *flags.TrimPath | ||||
| 	if *flags.Shared { | ||||
| 	ctxt.Flag_dynlink = *flags.Dynlink | ||||
| 	if *flags.Shared || *flags.Dynlink { | ||||
| 		ctxt.Flag_shared = 1 | ||||
| 	} | ||||
| 	ctxt.Bso = obj.Binitw(os.Stdout) | ||||
|  |  | |||
|  | @ -222,15 +222,22 @@ func Main() { | |||
| 	obj.Flagcount("x", "debug lexer", &Debug['x']) | ||||
| 	obj.Flagcount("y", "debug declarations in canned imports (with -d)", &Debug['y']) | ||||
| 	var flag_shared int | ||||
| 	var flag_dynlink bool | ||||
| 	if Thearch.Thechar == '6' { | ||||
| 		obj.Flagcount("largemodel", "generate code that assumes a large memory model", &flag_largemodel) | ||||
| 		obj.Flagcount("shared", "generate code that can be linked into a shared library", &flag_shared) | ||||
| 		flag.BoolVar(&flag_dynlink, "dynlink", false, "support references to Go symbols defined in other shared libraries") | ||||
| 	} | ||||
| 
 | ||||
| 	obj.Flagstr("cpuprofile", "file: write cpu profile to file", &cpuprofile) | ||||
| 	obj.Flagstr("memprofile", "file: write memory profile to file", &memprofile) | ||||
| 	obj.Flagparse(usage) | ||||
| 
 | ||||
| 	if flag_dynlink { | ||||
| 		flag_shared = 1 | ||||
| 	} | ||||
| 	Ctxt.Flag_shared = int32(flag_shared) | ||||
| 	Ctxt.Flag_dynlink = flag_dynlink | ||||
| 
 | ||||
| 	Ctxt.Debugasm = int32(Debug['S']) | ||||
| 	Ctxt.Debugvlog = int32(Debug['v']) | ||||
| 
 | ||||
|  |  | |||
|  | @ -476,8 +476,8 @@ func relocsym(s *LSym) { | |||
| 			} | ||||
| 
 | ||||
| 			// r->sym can be null when CALL $(constant) is transformed from absolute PC to relative PC call. | ||||
| 		case R_CALL, R_PCREL: | ||||
| 			if Linkmode == LinkExternal && r.Sym != nil && r.Sym.Type != SCONST && r.Sym.Sect != Ctxt.Cursym.Sect { | ||||
| 		case R_CALL, R_GOTPCREL, R_PCREL: | ||||
| 			if Linkmode == LinkExternal && r.Sym != nil && r.Sym.Type != SCONST && (r.Sym.Sect != Ctxt.Cursym.Sect || r.Type == R_GOTPCREL) { | ||||
| 				r.Done = 0 | ||||
| 
 | ||||
| 				// set up addend for eventual relocation via outer symbol. | ||||
|  |  | |||
|  | @ -235,6 +235,7 @@ const ( | |||
| 	R_PLT2 | ||||
| 	R_USEFIELD | ||||
| 	R_POWER_TOC | ||||
| 	R_GOTPCREL | ||||
| ) | ||||
| 
 | ||||
| // Reloc.variant | ||||
|  |  | |||
|  | @ -173,6 +173,9 @@ const ( | |||
| 	NAME_STATIC | ||||
| 	NAME_AUTO | ||||
| 	NAME_PARAM | ||||
| 	// A reference to name@GOT(SB) is a reference to the entry in the global offset | ||||
| 	// table for 'name'. | ||||
| 	NAME_GOTREF | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
|  | @ -380,6 +383,7 @@ const ( | |||
| 	R_PLT2 | ||||
| 	R_USEFIELD | ||||
| 	R_POWER_TOC | ||||
| 	R_GOTPCREL | ||||
| ) | ||||
| 
 | ||||
| type Auto struct { | ||||
|  | @ -431,6 +435,7 @@ type Link struct { | |||
| 	Debugdivmod        int32 | ||||
| 	Debugpcln          int32 | ||||
| 	Flag_shared        int32 | ||||
| 	Flag_dynlink       bool | ||||
| 	Bso                *Biobuf | ||||
| 	Pathname           string | ||||
| 	Windows            int32 | ||||
|  |  | |||
|  | @ -477,6 +477,9 @@ func Mconv(a *Addr) string { | |||
| 	case NAME_EXTERN: | ||||
| 		str = fmt.Sprintf("%s%s(SB)", a.Sym.Name, offConv(a.Offset)) | ||||
| 
 | ||||
| 	case NAME_GOTREF: | ||||
| 		str = fmt.Sprintf("%s%s@GOT(SB)", a.Sym.Name, offConv(a.Offset)) | ||||
| 
 | ||||
| 	case NAME_STATIC: | ||||
| 		str = fmt.Sprintf("%s<>%s(SB)", a.Sym.Name, offConv(a.Offset)) | ||||
| 
 | ||||
|  |  | |||
|  | @ -2028,6 +2028,7 @@ func oclass(ctxt *obj.Link, p *obj.Prog, a *obj.Addr) int { | |||
| 	case obj.TYPE_ADDR: | ||||
| 		switch a.Name { | ||||
| 		case obj.NAME_EXTERN, | ||||
| 			obj.NAME_GOTREF, | ||||
| 			obj.NAME_STATIC: | ||||
| 			if a.Sym != nil && isextern(a.Sym) || p.Mode == 32 { | ||||
| 				return Yi32 | ||||
|  | @ -2437,6 +2438,7 @@ func vaddr(ctxt *obj.Link, p *obj.Prog, a *obj.Addr, r *obj.Reloc) int64 { | |||
| 
 | ||||
| 	switch a.Name { | ||||
| 	case obj.NAME_STATIC, | ||||
| 		obj.NAME_GOTREF, | ||||
| 		obj.NAME_EXTERN: | ||||
| 		s := a.Sym | ||||
| 		if r == nil { | ||||
|  | @ -2444,7 +2446,10 @@ func vaddr(ctxt *obj.Link, p *obj.Prog, a *obj.Addr, r *obj.Reloc) int64 { | |||
| 			log.Fatalf("reloc") | ||||
| 		} | ||||
| 
 | ||||
| 		if isextern(s) || p.Mode != 64 { | ||||
| 		if a.Name == obj.NAME_GOTREF { | ||||
| 			r.Siz = 4 | ||||
| 			r.Type = obj.R_GOTPCREL | ||||
| 		} else if isextern(s) || p.Mode != 64 { | ||||
| 			r.Siz = 4 | ||||
| 			r.Type = obj.R_ADDR | ||||
| 		} else { | ||||
|  | @ -2519,6 +2524,7 @@ func asmandsz(ctxt *obj.Link, p *obj.Prog, a *obj.Addr, r int, rex int, m64 int) | |||
| 		base := int(a.Reg) | ||||
| 		switch a.Name { | ||||
| 		case obj.NAME_EXTERN, | ||||
| 			obj.NAME_GOTREF, | ||||
| 			obj.NAME_STATIC: | ||||
| 			if !isextern(a.Sym) && p.Mode == 64 { | ||||
| 				goto bad | ||||
|  | @ -2564,6 +2570,7 @@ func asmandsz(ctxt *obj.Link, p *obj.Prog, a *obj.Addr, r int, rex int, m64 int) | |||
| 	base = int(a.Reg) | ||||
| 	switch a.Name { | ||||
| 	case obj.NAME_STATIC, | ||||
| 		obj.NAME_GOTREF, | ||||
| 		obj.NAME_EXTERN: | ||||
| 		if a.Sym == nil { | ||||
| 			ctxt.Diag("bad addr: %v", p) | ||||
|  | @ -2582,7 +2589,10 @@ func asmandsz(ctxt *obj.Link, p *obj.Prog, a *obj.Addr, r int, rex int, m64 int) | |||
| 
 | ||||
| 	ctxt.Rexflag |= regrex[base]&Rxb | rex | ||||
| 	if base == REG_NONE || (REG_CS <= base && base <= REG_GS) || base == REG_TLS { | ||||
| 		if (a.Sym == nil || !isextern(a.Sym)) && base == REG_NONE && (a.Name == obj.NAME_STATIC || a.Name == obj.NAME_EXTERN) || p.Mode != 64 { | ||||
| 		if (a.Sym == nil || !isextern(a.Sym)) && base == REG_NONE && (a.Name == obj.NAME_STATIC || a.Name == obj.NAME_EXTERN || a.Name == obj.NAME_GOTREF) || p.Mode != 64 { | ||||
| 			if a.Name == obj.NAME_GOTREF && (a.Offset != 0 || a.Index != 0 || a.Scale != 0) { | ||||
| 				ctxt.Diag("%v has offset against gotref", p) | ||||
| 			} | ||||
| 			ctxt.Andptr[0] = byte(0<<6 | 5<<0 | r<<3) | ||||
| 			ctxt.Andptr = ctxt.Andptr[1:] | ||||
| 			goto putrelv | ||||
|  |  | |||
|  | @ -297,6 +297,84 @@ func progedit(ctxt *obj.Link, p *obj.Prog) { | |||
| 			p.From.Offset = 0 | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if ctxt.Flag_dynlink { | ||||
| 		if p.As == ALEAQ && p.From.Type == obj.TYPE_MEM && p.From.Name == obj.NAME_EXTERN { | ||||
| 			p.As = AMOVQ | ||||
| 			p.From.Type = obj.TYPE_ADDR | ||||
| 		} | ||||
| 		if p.From.Type == obj.TYPE_ADDR && p.From.Name == obj.NAME_EXTERN { | ||||
| 			if p.As != AMOVQ { | ||||
| 				ctxt.Diag("do not know how to handle TYPE_ADDR in %v with -dynlink", p) | ||||
| 			} | ||||
| 			if p.To.Type != obj.TYPE_REG { | ||||
| 				ctxt.Diag("do not know how to handle LEAQ-type insn to non-register in %v with -dynlink", p) | ||||
| 			} | ||||
| 			p.From.Type = obj.TYPE_MEM | ||||
| 			p.From.Name = obj.NAME_GOTREF | ||||
| 			if p.From.Offset != 0 { | ||||
| 				q := obj.Appendp(ctxt, p) | ||||
| 				q.As = AADDQ | ||||
| 				q.From.Type = obj.TYPE_CONST | ||||
| 				q.From.Offset = p.From.Offset | ||||
| 				q.To = p.To | ||||
| 				p.From.Offset = 0 | ||||
| 			} | ||||
| 		} | ||||
| 		if p.From3.Name == obj.NAME_EXTERN { | ||||
| 			ctxt.Diag("don't know how to handle %v with -dynlink", p) | ||||
| 		} | ||||
| 		if p.To2.Name == obj.NAME_EXTERN { | ||||
| 			ctxt.Diag("don't know how to handle %v with -dynlink", p) | ||||
| 		} | ||||
| 		var source *obj.Addr | ||||
| 		if p.From.Name == obj.NAME_EXTERN { | ||||
| 			if p.To.Name == obj.NAME_EXTERN { | ||||
| 				ctxt.Diag("cannot handle NAME_EXTERN on both sides in %v with -dynlink", p) | ||||
| 			} | ||||
| 			source = &p.From | ||||
| 		} else if p.To.Name == obj.NAME_EXTERN { | ||||
| 			source = &p.To | ||||
| 		} else { | ||||
| 			return | ||||
| 		} | ||||
| 		if p.As == obj.ATEXT || p.As == obj.AFUNCDATA || p.As == obj.ACALL || p.As == obj.ARET || p.As == obj.AJMP { | ||||
| 			return | ||||
| 		} | ||||
| 		if source.Type != obj.TYPE_MEM { | ||||
| 			ctxt.Diag("don't know how to handle %v with -dynlink", p) | ||||
| 		} | ||||
| 		p1 := obj.Appendp(ctxt, p) | ||||
| 		p2 := obj.Appendp(ctxt, p1) | ||||
| 
 | ||||
| 		p1.As = AMOVQ | ||||
| 		p1.From.Type = obj.TYPE_MEM | ||||
| 		p1.From.Sym = source.Sym | ||||
| 		p1.From.Name = obj.NAME_GOTREF | ||||
| 		p1.To.Type = obj.TYPE_REG | ||||
| 		p1.To.Reg = REG_R15 | ||||
| 
 | ||||
| 		p2.As = p.As | ||||
| 		p2.From = p.From | ||||
| 		p2.To = p.To | ||||
| 		if p.From.Name == obj.NAME_EXTERN { | ||||
| 			p2.From.Reg = REG_R15 | ||||
| 			p2.From.Name = obj.NAME_NONE | ||||
| 			p2.From.Sym = nil | ||||
| 		} else if p.To.Name == obj.NAME_EXTERN { | ||||
| 			p2.To.Reg = REG_R15 | ||||
| 			p2.To.Name = obj.NAME_NONE | ||||
| 			p2.To.Sym = nil | ||||
| 		} else { | ||||
| 			return | ||||
| 		} | ||||
| 		l := p.Link | ||||
| 		l2 := p2.Link | ||||
| 		*p = *p1 | ||||
| 		*p1 = *p2 | ||||
| 		p.Link = l | ||||
| 		p1.Link = l2 | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func nacladdr(ctxt *obj.Link, p *obj.Prog, a *obj.Addr) { | ||||
|  |  | |||
							
								
								
									
										167
									
								
								src/cmd/internal/obj/x86/obj6_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								src/cmd/internal/obj/x86/obj6_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,167 @@ | |||
| package x86_test | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"go/build" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path/filepath" | ||||
| 	"regexp" | ||||
| 	"runtime" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| const testdata = ` | ||||
| MOVQ AX, AX -> MOVQ AX, AX | ||||
| 
 | ||||
| LEAQ name(SB), AX -> MOVQ name@GOT(SB), AX | ||||
| LEAQ name+10(SB), AX -> MOVQ name@GOT(SB), AX; ADDQ $10, AX | ||||
| MOVQ $name(SB), AX -> MOVQ name@GOT(SB), AX | ||||
| MOVQ $name+10(SB), AX -> MOVQ name@GOT(SB), AX; ADDQ $10, AX | ||||
| 
 | ||||
| MOVQ name(SB), AX -> MOVQ name@GOT(SB), R15; MOVQ (R15), AX | ||||
| MOVQ name+10(SB), AX -> MOVQ name@GOT(SB), R15; MOVQ 10(R15), AX | ||||
| 
 | ||||
| CMPQ name(SB), $0 -> MOVQ name@GOT(SB), R15; CMPQ (R15), $0 | ||||
| 
 | ||||
| MOVQ $1, name(SB) -> MOVQ name@GOT(SB), R15; MOVQ $1, (R15) | ||||
| MOVQ $1, name+10(SB) -> MOVQ name@GOT(SB), R15; MOVQ $1, 10(R15) | ||||
| ` | ||||
| 
 | ||||
| type ParsedTestData struct { | ||||
| 	input              string | ||||
| 	marks              []int | ||||
| 	marker_to_input    map[int][]string | ||||
| 	marker_to_expected map[int][]string | ||||
| 	marker_to_output   map[int][]string | ||||
| } | ||||
| 
 | ||||
| const marker_start = 1234 | ||||
| 
 | ||||
| func parseTestData(t *testing.T) *ParsedTestData { | ||||
| 	r := &ParsedTestData{} | ||||
| 	scanner := bufio.NewScanner(strings.NewReader(testdata)) | ||||
| 	r.marker_to_input = make(map[int][]string) | ||||
| 	r.marker_to_expected = make(map[int][]string) | ||||
| 	marker := marker_start | ||||
| 	input_insns := []string{} | ||||
| 	for scanner.Scan() { | ||||
| 		line := scanner.Text() | ||||
| 		if len(strings.TrimSpace(line)) == 0 { | ||||
| 			continue | ||||
| 		} | ||||
| 		parts := strings.Split(line, "->") | ||||
| 		if len(parts) != 2 { | ||||
| 			t.Fatalf("malformed line %v", line) | ||||
| 		} | ||||
| 		r.marks = append(r.marks, marker) | ||||
| 		marker_insn := fmt.Sprintf("MOVQ $%d, AX", marker) | ||||
| 		input_insns = append(input_insns, marker_insn) | ||||
| 		for _, input_insn := range strings.Split(parts[0], ";") { | ||||
| 			input_insns = append(input_insns, input_insn) | ||||
| 			r.marker_to_input[marker] = append(r.marker_to_input[marker], normalize(input_insn)) | ||||
| 		} | ||||
| 		for _, expected_insn := range strings.Split(parts[1], ";") { | ||||
| 			r.marker_to_expected[marker] = append(r.marker_to_expected[marker], normalize(expected_insn)) | ||||
| 		} | ||||
| 		marker++ | ||||
| 	} | ||||
| 	r.input = "TEXT ·foo(SB),$0\n" + strings.Join(input_insns, "\n") + "\n" | ||||
| 	return r | ||||
| } | ||||
| 
 | ||||
| var spaces_re *regexp.Regexp = regexp.MustCompile("\\s+") | ||||
| var marker_re *regexp.Regexp = regexp.MustCompile("MOVQ \\$([0-9]+), AX") | ||||
| 
 | ||||
| func normalize(s string) string { | ||||
| 	return spaces_re.ReplaceAllLiteralString(strings.TrimSpace(s), " ") | ||||
| } | ||||
| 
 | ||||
| func asmOutput(t *testing.T, s string) []byte { | ||||
| 	tmpdir, err := ioutil.TempDir("", "progedittest") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmpdir) | ||||
| 	tmpfile, err := os.Create(filepath.Join(tmpdir, "input.s")) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	defer tmpfile.Close() | ||||
| 	_, err = tmpfile.WriteString(s) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	cmd := exec.Command( | ||||
| 		build.Default.GOROOT+"/bin/go", "tool", "asm", "-S", "-dynlink", | ||||
| 		"-o", filepath.Join(tmpdir, "output.6"), tmpfile.Name()) | ||||
| 
 | ||||
| 	var env []string | ||||
| 	for _, v := range os.Environ() { | ||||
| 		if !strings.HasPrefix(v, "GOARCH=") { | ||||
| 			env = append(env, v) | ||||
| 		} | ||||
| 	} | ||||
| 	cmd.Env = append(env, "GOARCH=amd64") | ||||
| 	asmout, err := cmd.CombinedOutput() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("error %s output %s", err, asmout) | ||||
| 	} | ||||
| 	return asmout | ||||
| } | ||||
| 
 | ||||
| func parseOutput(t *testing.T, td *ParsedTestData, asmout []byte) { | ||||
| 	scanner := bufio.NewScanner(bytes.NewReader(asmout)) | ||||
| 	marker := regexp.MustCompile("MOVQ \\$([0-9]+), AX") | ||||
| 	mark := -1 | ||||
| 	td.marker_to_output = make(map[int][]string) | ||||
| 	for scanner.Scan() { | ||||
| 		line := scanner.Text() | ||||
| 		if line[0] != '\t' { | ||||
| 			continue | ||||
| 		} | ||||
| 		parts := strings.SplitN(line, "\t", 3) | ||||
| 		if len(parts) != 3 { | ||||
| 			continue | ||||
| 		} | ||||
| 		n := normalize(parts[2]) | ||||
| 		mark_matches := marker.FindStringSubmatch(n) | ||||
| 		if mark_matches != nil { | ||||
| 			mark, _ = strconv.Atoi(mark_matches[1]) | ||||
| 			if _, ok := td.marker_to_input[mark]; !ok { | ||||
| 				t.Fatalf("unexpected marker %d", mark) | ||||
| 			} | ||||
| 		} else if mark != -1 { | ||||
| 			td.marker_to_output[mark] = append(td.marker_to_output[mark], n) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestDynlink(t *testing.T) { | ||||
| 	if runtime.GOOS == "nacl" || runtime.GOOS == "android" || (runtime.GOOS == "darwin" && runtime.GOARCH == "arm") { | ||||
| 		// iOS and nacl cannot fork | ||||
| 		t.Skipf("skipping on %s/%s", runtime.GOOS, runtime.GOARCH) | ||||
| 	} | ||||
| 	testdata := parseTestData(t) | ||||
| 	asmout := asmOutput(t, testdata.input) | ||||
| 	parseOutput(t, testdata, asmout) | ||||
| 	for _, m := range testdata.marks { | ||||
| 		i := strings.Join(testdata.marker_to_input[m], "; ") | ||||
| 		o := strings.Join(testdata.marker_to_output[m], "; ") | ||||
| 		e := strings.Join(testdata.marker_to_expected[m], "; ") | ||||
| 		if o != e { | ||||
| 			if o == i { | ||||
| 				t.Errorf("%s was unchanged; should have become %s", i, e) | ||||
| 			} else { | ||||
| 				t.Errorf("%s became %s; should have become %s", i, o, e) | ||||
| 			} | ||||
| 		} else if i != e { | ||||
| 			t.Logf("%s correctly became %s", i, o) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Michael Hudson-Doyle
						Michael Hudson-Doyle