-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
096f0f4
commit d7db192
Showing
4 changed files
with
363 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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("Не удалось обнаружить положительных изменений в результирующем профиле") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters