From d7db1922d91ecc7a3cc3aea5d0e0b922bbc80ed5 Mon Sep 17 00:00:00 2001 From: bbrodriges Date: Wed, 12 Jul 2023 12:32:49 +0300 Subject: [PATCH] add increments 16,17,18 --- cmd/shortenertestbeta/iteration16_test.go | 102 +++++++++++++ cmd/shortenertestbeta/iteration17_test.go | 72 +++++++++ cmd/shortenertestbeta/iteration18_test.go | 174 ++++++++++++++++++++++ cmd/shortenertestbeta/main_test.go | 15 ++ 4 files changed, 363 insertions(+) create mode 100644 cmd/shortenertestbeta/iteration16_test.go create mode 100644 cmd/shortenertestbeta/iteration17_test.go create mode 100644 cmd/shortenertestbeta/iteration18_test.go diff --git a/cmd/shortenertestbeta/iteration16_test.go b/cmd/shortenertestbeta/iteration16_test.go new file mode 100644 index 0000000..b939537 --- /dev/null +++ b/cmd/shortenertestbeta/iteration16_test.go @@ -0,0 +1,102 @@ +package main + +// Basic imports +import ( + "context" + "os" + "os/exec" + "strings" + "time" + + "github.com/google/pprof/profile" + "github.com/stretchr/testify/suite" +) + +// Iteration16Suite является сьютом с тестами и состоянием для инкремента +type Iteration16Suite struct { + suite.Suite +} + +// SetupSuite подготавливает необходимые зависимости +func (suite *Iteration16Suite) SetupSuite() { + // check required flags + suite.Require().NotEmpty(flagTargetSourcePath, "-source-path non-empty flag required") + // pprof flags + // suite.Require().NotEmpty(flagBaseProfilePath, "-base-profile-path non-empty flag required") + // suite.Require().NotEmpty(flagResultProfilePath, "-result-profile-path non-empty flag required") + // suite.Require().NotEmpty(flagPackageName, "-package-name non-empty flag required") +} + +// TestBenchmarksPresence пробует запустить бенчмарки и получить результаты используя стандартный тулинг +func (suite *Iteration16Suite) TestBenchmarksPresence() { + sourcePath := strings.TrimRight(flagTargetSourcePath, "/") + "/..." + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + // запускаем команду стандартного тулинга + cmd := exec.CommandContext(ctx, "go", "test", "-bench=.", "-benchmem", "-benchtime=100ms", "-run=^$", sourcePath) + cmd.Env = os.Environ() // pass parent envs + out, err := cmd.CombinedOutput() + suite.Assert().NoError(err, "Невозможно получить результат выполнения команды: %s. Вывод:\n\n %s", cmd, out) + + // проверяем наличие в выводе ключевых слов + matched := strings.Contains(string(out), "ns/op") && strings.Contains(string(out), "B/op") + found := suite.Assert().True(matched, "Отсутствует информация о наличии бенчмарков в коде, команда: %s", cmd) + + if !found { + suite.T().Logf("Вывод команды:\n\n%s", string(out)) + } +} + +// TestProfilesDiff пробует получить разницу между двумя результатами запуска pprof +func (suite *Iteration16Suite) TestProfilesDiff() { + // тест пока не работает + suite.T().Skip("not implemented") + + // открываем базовый профиль + baseFd, err := os.Open(flagBaseProfilePath) + suite.Require().NoError(err, "Невозможно открыть файл с базовым профилем: %s", flagBaseProfilePath) + defer baseFd.Close() + + // открываем новый профиль + resultFd, err := os.Open(flagResultProfilePath) + suite.Require().NoError(err, "Невозможно открыть файл с результирующим профилем: %s", flagResultProfilePath) + defer resultFd.Close() + + // парсим профили + baseProfile, err := profile.Parse(baseFd) + suite.Assert().NoError(err, "Невозможно распарсить базовый профиль") + + resultProfile, err := profile.Parse(resultFd) + suite.Assert().NoError(err, "Невозможно распарсить результирующий профиль") + + // инвертируем значения базового профиля, чтобы получить положительную динамику + baseProfile.Scale(-1) + mergedProfile, err := profile.Merge([]*profile.Profile{resultProfile, baseProfile}) + + // проверяем только функции нашего пакета + for i, sample := range mergedProfile.Sample { + if len(mergedProfile.Function) < i { + break + } + + fn := mergedProfile.Function[i] + fName := strings.ToLower(fn.Name) + + // пропускаем тестовые функции + if !strings.Contains(fName, flagPackageName) || + strings.Contains(fName, "test_run") { + continue + } + + for _, value := range sample.Value { + // нашли улучшение + if value < 0 { + return + } + } + } + + suite.T().Error("Не удалось обнаружить положительных изменений в результирующем профиле") +} diff --git a/cmd/shortenertestbeta/iteration17_test.go b/cmd/shortenertestbeta/iteration17_test.go new file mode 100644 index 0000000..feae01b --- /dev/null +++ b/cmd/shortenertestbeta/iteration17_test.go @@ -0,0 +1,72 @@ +package main + +// Basic imports +import ( + "context" + "fmt" + "os" + "os/exec" + "time" + + "github.com/stretchr/testify/suite" +) + +// Iteration17Suite является сьютом с тестами и состоянием для инкремента +type Iteration17Suite struct { + suite.Suite +} + +// SetupSuite подготавливает необходимые зависимости +func (suite *Iteration17Suite) SetupSuite() { + suite.Require().NotEmpty(flagTargetSourcePath, "-source-path non-empty flag required") +} + +// TestStylingDiff пробует проверить правильность форматирования кода в проекте +func (suite *Iteration17Suite) TestStylingDiff() { + // проверяем форматирование с помощью gofmt + gofmtErr := checkGofmtStyling(flagTargetSourcePath) + // проверяем форматирование с помощью goimports + goimportsErr := checkGoimportsStyling(flagTargetSourcePath) + + // нас устраивает любой один форматтер, которые не вернул ошибку + if gofmtErr == nil || goimportsErr == nil { + return + } + + suite.Assert().NoError(gofmtErr, "Ошибка проверки форматирования с помощью gofmt") + suite.Assert().NoError(goimportsErr, "Ошибка проверки форматирования с помощью goimports") +} + +// checkGofmtStyling возвращает ошибку, если файл не отформатирован согласно правилам gofmt +func checkGofmtStyling(path string) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "gofmt", "-l", "-s", path) + cmd.Env = os.Environ() // pass parent envs + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("Невозможно получить результат выполнения команды: %s. Ошибка: %w", cmd, err) + } + if len(out) > 0 { + return fmt.Errorf("Найдены неотформатированные файлы:\n\n%s", cmd) + } + return nil +} + +// checkGoimportsStyling возвращает ошибку, если файл не отформатирован согласно правилам goimports +func checkGoimportsStyling(path string) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "goimports", "-l", path) + cmd.Env = os.Environ() // pass parent envs + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("Невозможно получить результат выполнения команды: %s. Ошибка: %w", cmd, err) + } + if len(out) > 0 { + return fmt.Errorf("Найдены неотформатированные файлы:\n\n%s", cmd) + } + return nil +} diff --git a/cmd/shortenertestbeta/iteration18_test.go b/cmd/shortenertestbeta/iteration18_test.go new file mode 100644 index 0000000..cf38303 --- /dev/null +++ b/cmd/shortenertestbeta/iteration18_test.go @@ -0,0 +1,174 @@ +package main + +// Basic imports +import ( + "errors" + "go/ast" + "go/parser" + "go/token" + "io/fs" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// Iteration18Suite является сьютом с тестами и состоянием для инкремента +type Iteration18Suite struct { + suite.Suite +} + +// SetupSuite подготавливает необходимые зависимости +func (suite *Iteration18Suite) SetupSuite() { + // check required flags + suite.Require().NotEmpty(flagTargetSourcePath, "-source-path non-empty flag required") +} + +// TestDocsComments пробует проверить налиция документационных комментариев в коде +func (suite *Iteration18Suite) TestDocsComments() { + var reports []string + err := filepath.WalkDir(flagTargetSourcePath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + // пропускаем служебные директории + if d.Name() == "vendor" || d.Name() == ".git" { + return filepath.SkipDir + } + // проваливаемся в директорию + return nil + } + + // проускаем не Go файлы и Go тесты + if !strings.HasSuffix(d.Name(), ".go") || + strings.HasSuffix(d.Name(), "_test.go") { + return nil + } + + reported := undocumentedNodes(suite.T(), path) + if len(reported) > 0 { + reports = append(reports, reported...) + } + + return nil + }) + + suite.NoError(err, "Неожиданная ошибка") + if len(reports) > 0 { + suite.Failf("Найдены файлы с недокументированной сущностями", + strings.Join(reports, "\n"), + ) + } +} + +// TestExamplePresence пробует рекурсивно найти хотя бы один файл example_test.go в директории с исходным кодом проекта +func (suite *Iteration18Suite) TestExamplePresence() { + err := filepath.WalkDir(flagTargetSourcePath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + // пропускаем служебные директории + if d.Name() == "vendor" || d.Name() == ".git" { + return filepath.SkipDir + } + // проваливаемся в директорию + return nil + } + + // проверяем имя файла + if strings.HasSuffix(d.Name(), "example_test.go") { + // возвращаем сигнальную ошибку + return errUsageFound + } + + return nil + }) + + // проверяем сигнальную ошибку + if errors.Is(err, errUsageFound) { + // найден хотя бы один файл + return + } + + if err == nil { + suite.T().Error("Не найден ни один файл example_test.go") + return + } + suite.T().Errorf("Неожиданная ошибка при поиске файла example_test.go: %s", err) +} + +func undocumentedNodes(t *testing.T, filepath string) []string { + t.Helper() + + fset := token.NewFileSet() + sf, err := parser.ParseFile(fset, filepath, nil, parser.ParseComments) + require.NoError(t, err) + + // пропускаем автоматически сгенерированные файлы + if isGenerated(sf) { + return nil + } + + var reports []string + + for _, decl := range sf.Decls { + switch node := decl.(type) { + case *ast.GenDecl: + if undocumentedGenDecl(node) { + reports = append(reports, fset.Position(node.Pos()).String()) + } + case *ast.FuncDecl: + if node.Name.IsExported() && node.Doc == nil { + reports = append(reports, fset.Position(node.Pos()).String()) + } + } + } + + return reports +} + +// undocumentedGenDecl проверяет, что экспортированная декларация является недокументированной +func undocumentedGenDecl(decl *ast.GenDecl) bool { + for _, spec := range decl.Specs { + switch st := spec.(type) { + case *ast.TypeSpec: + if st.Name.IsExported() && decl.Doc == nil { + return true + } + case *ast.ValueSpec: + for _, name := range st.Names { + if name.IsExported() && decl.Doc == nil { + return true + } + } + } + } + return false +} + +// isGenerated проверяет сгенерирован ли файл автоматически +// на основании правил, описанных в https://golang.org/s/generatedcode. +func isGenerated(file *ast.File) bool { + const ( + genCommentPrefix = "// Code generated " + genCommentSuffix = " DO NOT EDIT." + ) + + for _, group := range file.Comments { + for _, comment := range group.List { + if strings.HasPrefix(comment.Text, genCommentPrefix) && + strings.HasSuffix(comment.Text, genCommentSuffix) && + len(comment.Text) > len(genCommentPrefix)+len(genCommentSuffix) { + return true + } + } + } + + return false +} diff --git a/cmd/shortenertestbeta/main_test.go b/cmd/shortenertestbeta/main_test.go index 3d9abe7..77aa8f9 100644 --- a/cmd/shortenertestbeta/main_test.go +++ b/cmd/shortenertestbeta/main_test.go @@ -88,3 +88,18 @@ func TestIteration15(t *testing.T) { // Запускает тест-сьют для пятнадцатой итерации suite.Run(t, new(Iteration15Suite)) } + +func TestIteration16(t *testing.T) { + // Запускает тест-сьют для шестнадцатой итерации + suite.Run(t, new(Iteration16Suite)) +} + +func TestIteration17(t *testing.T) { + // Запускает тест-сьют для семнадцатой итерации + suite.Run(t, new(Iteration17Suite)) +} + +func TestIteration18(t *testing.T) { + // Запускает тест-сьют для восемнадцатой итерации + suite.Run(t, new(Iteration18Suite)) +}