Squashed commit of the following:
commit 347ba11cfe4a49bff6fc29063b49416d90525e52 Author: Fedor Korotkiy <prime@yandex-team.ru> Date: Sat Feb 8 22:44:26 2020 +0300 Sandboxed test execution. commit c5c9557dd59c54971a78d424ec118655f6b2005c Author: Fedor Korotkiy <prime@yandex-team.ru> Date: Sat Feb 8 21:13:13 2020 +0300 Fix paths used during testing. commit 1ba21eb0aad08f543c6a99bfd927721207943abb Author: Fedor Korotkiy <prime@yandex-team.ru> Date: Sat Feb 8 20:56:32 2020 +0300 Helper for process sandboxing commit 54f0aa11156c1d2c998a060b60be7af8666d5da4 Author: Fedor Korotkiy <prime@yandex-team.ru> Date: Sat Feb 8 20:10:56 2020 +0300 Package list helper.
This commit is contained in:
parent
3290d0880c
commit
0de2390008
15 changed files with 257 additions and 53 deletions
|
@ -1,10 +1,45 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/packages"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// getPackageFiles returns absolute paths for all files in rootPackage and it's subpackages
|
||||||
|
// including tests and non-go files.
|
||||||
|
func getPackageFiles(rootPackage string, buildFlags []string) map[string]struct{} {
|
||||||
|
cfg := &packages.Config{
|
||||||
|
Dir: rootPackage,
|
||||||
|
Mode: packages.NeedFiles,
|
||||||
|
BuildFlags: buildFlags,
|
||||||
|
Tests: true,
|
||||||
|
}
|
||||||
|
pkgs, err := packages.Load(cfg, "./...")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("unable to load packages %s: %s", rootPackage, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if packages.PrintErrors(pkgs) > 0 {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
files := make(map[string]struct{})
|
||||||
|
for _, p := range pkgs {
|
||||||
|
for _, f := range p.GoFiles {
|
||||||
|
files[f] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, f := range p.OtherFiles {
|
||||||
|
files[f] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
// listTestFiles returns absolute paths for all _test.go files of the package
|
// listTestFiles returns absolute paths for all _test.go files of the package
|
||||||
// including the ones with "private" build tag.
|
// including the ones with "private" build tag.
|
||||||
func listTestFiles(rootPackage string) []string {
|
func listTestFiles(rootPackage string) []string {
|
||||||
|
@ -53,3 +88,38 @@ func listPrivateFiles(rootPackage string) []string {
|
||||||
sort.Strings(files)
|
sort.Strings(files)
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listTestsAndBinaries(rootDir string, buildFlags []string) (binaries, tests map[string]struct{}) {
|
||||||
|
cfg := &packages.Config{
|
||||||
|
Dir: rootDir,
|
||||||
|
Mode: packages.NeedName | packages.NeedFiles,
|
||||||
|
BuildFlags: buildFlags,
|
||||||
|
Tests: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgs, err := packages.Load(cfg, "./...")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("unable to load packages %s: %s", rootDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if packages.PrintErrors(pkgs) > 0 {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests = map[string]struct{}{}
|
||||||
|
binaries = map[string]struct{}{}
|
||||||
|
|
||||||
|
for _, p := range pkgs {
|
||||||
|
if p.Name != "main" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(p.PkgPath, ".test") {
|
||||||
|
tests[strings.TrimSuffix(p.PkgPath, ".test")] = struct{}{}
|
||||||
|
} else {
|
||||||
|
binaries[p.PkgPath] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,3 +34,18 @@ func TestPrivateFiles(t *testing.T) {
|
||||||
absPaths([]string{"sum/private_test.go", "sum/solution.go"}),
|
absPaths([]string{"sum/private_test.go", "sum/solution.go"}),
|
||||||
listPrivateFiles("../testdata/list"))
|
listPrivateFiles("../testdata/list"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListPackages(t *testing.T) {
|
||||||
|
binaries, tests := listTestsAndBinaries("../testdata/pkgfind/task", []string{"-tags", "private"})
|
||||||
|
|
||||||
|
assert.Equal(t, binaries, map[string]struct{}{
|
||||||
|
"gitlab.com/slon/shad-go/task/cmd/tool": {},
|
||||||
|
"gitlab.com/slon/shad-go/task/cmd/tool_with_test": {},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, tests, map[string]struct{}{
|
||||||
|
"gitlab.com/slon/shad-go/task/cmd/tool_with_test": {},
|
||||||
|
"gitlab.com/slon/shad-go/task/pkg/a": {},
|
||||||
|
"gitlab.com/slon/shad-go/task/pkg/c": {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
39
tools/testtool/commands/sandbox.go
Normal file
39
tools/testtool/commands/sandbox.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func currentUserIsRoot() bool {
|
||||||
|
me, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return me.Uid == "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
func sandbox(cmd *exec.Cmd) error {
|
||||||
|
nobody, err := user.Lookup("nobody")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, _ := strconv.Atoi(nobody.Uid)
|
||||||
|
gid, _ := strconv.Atoi(nobody.Gid)
|
||||||
|
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Credential: &syscall.Credential{
|
||||||
|
Uid: uint32(uid),
|
||||||
|
Gid: uint32(gid),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Env = []string{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
16
tools/testtool/commands/sandbox_test.go
Normal file
16
tools/testtool/commands/sandbox_test.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSandbox(t *testing.T) {
|
||||||
|
var cmd exec.Cmd
|
||||||
|
|
||||||
|
require.NoError(t, sandbox(&cmd))
|
||||||
|
require.True(t, cmd.SysProcAttr.Credential.Uid > 0)
|
||||||
|
require.True(t, cmd.SysProcAttr.Credential.Gid > 0)
|
||||||
|
}
|
|
@ -1,15 +1,18 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/tools/go/packages"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -17,7 +20,8 @@ const (
|
||||||
studentRepoFlag = "student-repo"
|
studentRepoFlag = "student-repo"
|
||||||
privateRepoFlag = "private-repo"
|
privateRepoFlag = "private-repo"
|
||||||
|
|
||||||
testdataDir = "testdata"
|
testdataDir = "testdata"
|
||||||
|
moduleImportPath = "gitlab.com/slon/shad-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testSubmissionCmd = &cobra.Command{
|
var testSubmissionCmd = &cobra.Command{
|
||||||
|
@ -81,55 +85,58 @@ func problemDirExists(repo, problem string) bool {
|
||||||
|
|
||||||
func testSubmission(studentRepo, privateRepo, problem string) {
|
func testSubmission(studentRepo, privateRepo, problem string) {
|
||||||
// Create temp directory to store all files required to test the solution.
|
// Create temp directory to store all files required to test the solution.
|
||||||
tmpDir, err := ioutil.TempDir("/tmp", problem+"-")
|
tmpRepo, err := ioutil.TempDir("/tmp", problem+"-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
if err := os.Chmod(tmpRepo, 0755); err != nil {
|
||||||
log.Printf("testing submission in %s", tmpDir)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = os.RemoveAll(tmpRepo) }()
|
||||||
|
log.Printf("testing submission in %s", tmpRepo)
|
||||||
|
|
||||||
// Path to student's problem folder.
|
|
||||||
studentProblem := path.Join(studentRepo, problem)
|
|
||||||
// Path to private problem folder.
|
// Path to private problem folder.
|
||||||
privateProblem := path.Join(privateRepo, problem)
|
privateProblem := path.Join(privateRepo, problem)
|
||||||
|
|
||||||
// Copy submission files to temp dir.
|
// Copy student repo files to temp dir.
|
||||||
log.Printf("copying student solution")
|
log.Printf("copying student repo")
|
||||||
copyContents(studentProblem, tmpDir)
|
copyContents(studentRepo, ".", tmpRepo)
|
||||||
|
|
||||||
// Copy tests from private repo to temp dir.
|
// Copy tests from private repo to temp dir.
|
||||||
log.Printf("copying tests")
|
log.Printf("copying tests")
|
||||||
tests := listTestFiles(privateProblem)
|
tests := listTestFiles(privateProblem)
|
||||||
copyFiles(privateProblem, relPaths(privateProblem, tests), tmpDir)
|
copyFiles(privateRepo, relPaths(privateRepo, tests), tmpRepo)
|
||||||
|
|
||||||
// Copy !change files from private repo to temp dir.
|
// Copy !change files from private repo to temp dir.
|
||||||
log.Printf("copying !change files")
|
log.Printf("copying !change files")
|
||||||
protected := listProtectedFiles(privateProblem)
|
protected := listProtectedFiles(privateProblem)
|
||||||
copyFiles(privateProblem, relPaths(privateProblem, protected), tmpDir)
|
copyFiles(privateRepo, relPaths(privateRepo, protected), tmpRepo)
|
||||||
|
|
||||||
// Copy testdata directory from private repo to temp dir.
|
// Copy testdata directory from private repo to temp dir.
|
||||||
log.Printf("copying testdata directory")
|
log.Printf("copying testdata directory")
|
||||||
copyDir(path.Join(privateProblem, testdataDir), tmpDir)
|
copyDir(privateRepo, path.Join(problem, testdataDir), tmpRepo)
|
||||||
|
|
||||||
// Copy go.mod and go.sum from private repo to temp dir.
|
// Copy go.mod and go.sum from private repo to temp dir.
|
||||||
log.Printf("copying go.mod and go.sum")
|
log.Printf("copying go.mod and go.sum")
|
||||||
copyFiles(privateRepo, []string{"go.mod", "go.sum"}, tmpDir)
|
copyFiles(privateRepo, []string{"go.mod", "go.sum"}, tmpRepo)
|
||||||
|
|
||||||
// Run tests.
|
// Run tests.
|
||||||
log.Printf("running tests")
|
log.Printf("running tests")
|
||||||
runTests(tmpDir)
|
runTests(tmpRepo, problem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyDir recursively copies src directory to dst.
|
// copyDir recursively copies src directory to dst.
|
||||||
func copyDir(src, dst string) {
|
func copyDir(baseDir, src, dst string) {
|
||||||
_, err := os.Stat(src)
|
_, err := os.Stat(src)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("rsync", "-r", src, dst)
|
cmd := exec.Command("rsync", "-prR", src, dst)
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Dir = baseDir
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
log.Fatalf("directory copying failed: %s", err)
|
log.Fatalf("directory copying failed: %s", err)
|
||||||
|
@ -137,8 +144,8 @@ func copyDir(src, dst string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyContents recursively copies src contents to dst.
|
// copyContents recursively copies src contents to dst.
|
||||||
func copyContents(src, dst string) {
|
func copyContents(baseDir, src, dst string) {
|
||||||
copyDir(src+"/", dst)
|
copyDir(baseDir, src+"/", dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyFiles copies files preserving directory structure relative to baseDir.
|
// copyFiles copies files preserving directory structure relative to baseDir.
|
||||||
|
@ -146,7 +153,7 @@ func copyContents(src, dst string) {
|
||||||
// Existing files get replaced.
|
// Existing files get replaced.
|
||||||
func copyFiles(baseDir string, relPaths []string, dst string) {
|
func copyFiles(baseDir string, relPaths []string, dst string) {
|
||||||
for _, p := range relPaths {
|
for _, p := range relPaths {
|
||||||
cmd := exec.Command("rsync", "-rR", p, dst)
|
cmd := exec.Command("rsync", "-prR", p, dst)
|
||||||
cmd.Dir = baseDir
|
cmd.Dir = baseDir
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
@ -157,48 +164,72 @@ func copyFiles(baseDir string, relPaths []string, dst string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runTests runs all tests in directory with race detector.
|
func randomName() string {
|
||||||
func runTests(testDir string) {
|
var raw [8]byte
|
||||||
cmd := exec.Command("go", "test", "-v", "-mod", "readonly", "-tags", "private", "-race", "./...")
|
_, _ = rand.Read(raw[:])
|
||||||
cmd.Env = append(os.Environ(), "GOFLAGS=")
|
return hex.EncodeToString(raw[:])
|
||||||
cmd.Dir = testDir
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
log.Fatalf("go test command failed: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPackageFiles returns absolute paths for all files in rootPackage and it's subpackages
|
// runTests runs all tests in directory with race detector.
|
||||||
// including tests and non-go files.
|
func runTests(testDir, problem string) {
|
||||||
func getPackageFiles(rootPackage string, buildFlags []string) map[string]struct{} {
|
binCache, err := ioutil.TempDir("/tmp", "bincache")
|
||||||
cfg := &packages.Config{
|
|
||||||
Dir: rootPackage,
|
|
||||||
Mode: packages.NeedFiles,
|
|
||||||
BuildFlags: buildFlags,
|
|
||||||
Tests: true,
|
|
||||||
}
|
|
||||||
pkgs, err := packages.Load(cfg, "./...")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("unable to load packages %s: %s", rootPackage, err)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Chmod(binCache, 0755); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if packages.PrintErrors(pkgs) > 0 {
|
runGo := func(arg ...string) {
|
||||||
os.Exit(1)
|
log.Printf("> go %s", strings.Join(arg, " "))
|
||||||
}
|
|
||||||
|
|
||||||
files := make(map[string]struct{})
|
cmd := exec.Command("go", arg...)
|
||||||
for _, p := range pkgs {
|
cmd.Env = append(os.Environ(), "GOFLAGS=")
|
||||||
for _, f := range p.GoFiles {
|
cmd.Dir = testDir
|
||||||
files[f] = struct{}{}
|
cmd.Stdout = os.Stdout
|
||||||
}
|
cmd.Stderr = os.Stderr
|
||||||
for _, f := range p.OtherFiles {
|
if err := cmd.Run(); err != nil {
|
||||||
files[f] = struct{}{}
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return files
|
binaries := map[string]string{}
|
||||||
|
testBinaries := map[string]string{}
|
||||||
|
|
||||||
|
binPkgs, testPkgs := listTestsAndBinaries(testDir, []string{"-tags", "private", "-mod", "readonly"})
|
||||||
|
for binaryPkg := range binPkgs {
|
||||||
|
binPath := filepath.Join(binCache, randomName())
|
||||||
|
binaries[binaryPkg] = binPath
|
||||||
|
runGo("build", "-mod", "readonly", "-tags", "private", "-o", binPath, binaryPkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
binariesJSON, _ := json.Marshal(binPkgs)
|
||||||
|
|
||||||
|
for testPkg := range testPkgs {
|
||||||
|
binPath := filepath.Join(binCache, randomName())
|
||||||
|
testBinaries[testPkg] = binPath
|
||||||
|
runGo("test", "-mod", "readonly", "-tags", "private", "-c", "-o", binPath, testPkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
for testPkg, testBinary := range testBinaries {
|
||||||
|
relPath := strings.TrimPrefix(testPkg, moduleImportPath)
|
||||||
|
|
||||||
|
cmd := exec.Command(testBinary)
|
||||||
|
if currentUserIsRoot() {
|
||||||
|
if err := sandbox(cmd); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Dir = filepath.Join(testDir, relPath)
|
||||||
|
cmd.Env = []string{"TESTTOOL_BINARIES=" + string(binariesJSON)}
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// relPaths converts paths to relative (to the baseDir) ones.
|
// relPaths converts paths to relative (to the baseDir) ones.
|
||||||
|
|
3
tools/testtool/testdata/pkgfind/go.mod
vendored
Normal file
3
tools/testtool/testdata/pkgfind/go.mod
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module gitlab.com/slon/shad-go
|
||||||
|
|
||||||
|
go 1.13
|
5
tools/testtool/testdata/pkgfind/task/cmd/tool/main.go
vendored
Normal file
5
tools/testtool/testdata/pkgfind/task/cmd/tool/main.go
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
}
|
5
tools/testtool/testdata/pkgfind/task/cmd/tool_with_test/main.go
vendored
Normal file
5
tools/testtool/testdata/pkgfind/task/cmd/tool_with_test/main.go
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
}
|
7
tools/testtool/testdata/pkgfind/task/cmd/tool_with_test/main_test.go
vendored
Normal file
7
tools/testtool/testdata/pkgfind/task/cmd/tool_with_test/main_test.go
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestExample(t *testing.T) {
|
||||||
|
|
||||||
|
}
|
1
tools/testtool/testdata/pkgfind/task/pkg/a/a.go
vendored
Normal file
1
tools/testtool/testdata/pkgfind/task/pkg/a/a.go
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package a
|
1
tools/testtool/testdata/pkgfind/task/pkg/a/a_test.go
vendored
Normal file
1
tools/testtool/testdata/pkgfind/task/pkg/a/a_test.go
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package a
|
1
tools/testtool/testdata/pkgfind/task/pkg/b/b.go
vendored
Normal file
1
tools/testtool/testdata/pkgfind/task/pkg/b/b.go
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package b
|
3
tools/testtool/testdata/pkgfind/task/pkg/c/c_test.go
vendored
Normal file
3
tools/testtool/testdata/pkgfind/task/pkg/c/c_test.go
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// +build private
|
||||||
|
|
||||||
|
package c_test
|
3
tools/testtool/testdata/submissions/correct/sum/student/sum/pkg/f.go
vendored
Normal file
3
tools/testtool/testdata/submissions/correct/sum/student/sum/pkg/f.go
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
func F() {}
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
package sum
|
package sum
|
||||||
|
|
||||||
|
import "gitlab.com/slon/shad-go/sum/pkg"
|
||||||
|
|
||||||
func Sum(a, b int64) int64 {
|
func Sum(a, b int64) int64 {
|
||||||
|
pkg.F()
|
||||||
return a + b
|
return a + b
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue