Параллельное табличное тестирование в go с треском провалилось - PullRequest
0 голосов
/ 08 апреля 2020

У меня есть следующая тестовая функция


func TestIntegrationAppsWithProductionSelf(t *testing.T) {
    // here is where the apps array that will act as my test suite is being populated
    myapps, err := RetrieveApps(fs)
    for _, v := range apps {
        v := v
        t.Run("", func(t *testing.T) {
            t.Parallel()
            expectedOutput = `=` + v + `
`
            cmpOpts.SingleApp = v
            t.Logf("\t\tTesting %s\n", v)
            buf, err := VarsCmp(output, cmpOpts)
            if err != nil {
                t.Fatalf("ERROR executing var comparison for %s: %s\n", v, err)
            }
            assert.Equal(t, expectedOutput, buf.String())
        })
    }
}

Тест не пройден, несмотря на то, что когда я удаляю t.Parallel() (даже сохраняя структуру субтестирования), он проходит успешно.

Ошибка (случается, как сказано выше, только когда включено t.Parallel()) связана с тем фактом, что сравниваемые значения, передаваемые в assert, не совпадают c, т. Е. Метод assert сравнивает значения, которые он не должен ' т)

Почему это так? Я также выполняю это крипти c переназначение переменной набора тестов (v := v), которую я не понимаю)

edit : Блуждающий, если это было использование assert метод из этого пакета, я произвел следующую замену, тем не менее, конечный результат такой же,

    //assert.Equal(t, expectedOutput, buf.String())
    if expectedOutput != buf.String() {
        t.Errorf("Failed! Expected %s - Actual: %s\n", expectedOutput, buf.String())
    }

Ответы [ 2 ]

2 голосов
/ 08 апреля 2020

Давайте разберем случай.

Сначала давайте обратимся к документам по testing.T.Run:

Выполнить прогоны f как подтест t, называемый имя. Он запускается f в отдельной goroutine <…>

(выделение мое.)

Так что, когда вы звоните t.Run("some_name", someFn), это SomeFn запускается набором тестов, как если бы вы вручную делали что-то вроде

go someFn(t)

Далее, давайте заметим, что вы не передаете именованную функцию при вызове t.Run, а скорее передаете ее так function literal; давайте процитируем язык spe c на них :

Функциональные литералы замыкания: они могут ссылаются на переменные, определенные в окружающей функции. Затем эти переменные распределяются между окружающей функцией и литералом функции и сохраняются до тех пор, пока они доступны.

В вашем случае это означает, что когда компилятор компилирует тело литерала функции, она заставляет функцию «закрывать» любую переменную, о которой упоминает ее тело, и которая не является одним из параметров формальной функции; в вашем случае единственным параметром функции является t *testing.T, следовательно, любая другая переменная, к которой осуществляется доступ, захватывается созданным замыканием.

В Go, когда литерал функции закрывается над переменной, он делает это, сохраняя ссылка на эту переменную - которая явно упоминается в spe c как («Эти переменные затем разделяются между окружающей функцией и литералом функции <…>», снова , выделено мое.)

Теперь обратите внимание, что циклы в Go повторно используют переменных итерации на каждой итерации; то есть, когда вы пишете

for _, v := range apps {

, эта переменная v создается один раз во "внешней" области видимости l oop и затем получает переназначенный на каждой итерации л oop. Напомним: той же переменной, чье хранилище находится в некоторой фиксированной точке в памяти, присваивается новое значение на каждой итерации.

Теперь, поскольку литерал функции закрывает внешние переменные, сохраняя ссылки для них - в отличие от копирования их значений во «время» его определения в , - без этого «навороченного» * ​​1057 * «трюка» каждого литерала функции, созданного при каждом вызове t.Run в вашем l oop будет указываться точно такая же итерационная переменная v из l oop.
Конструкция v := v объявляет другую переменную с именем v, которая является local для Тело l oop и в то же время присваивает ему значение итерационной переменной l oop v. Поскольку локальный v "shadows" l oop итератора v, объявленный впоследствии литерал функции будет закрываться по этой локальной переменной, и, следовательно, каждый литерал функции, созданный на каждой итерации, будет закрываться по отдельной отдельной переменной v .

Зачем это нужно, спросите вы?

Это необходимо из-за тонкой проблемы с взаимодействием итерационной переменной l oop и goroutines, которая подробно описана на Go wiki : когда кто-то делает что-то вроде

for _, v := range apps {
  go func() {
    // use v
  }()
}

Создается литерал функции, закрывающий v, и затем он запускается с помощью оператора go - параллельно с goroutine, который запускает l oop, а все остальные goroutines запускаются на других len(apps)-1 итерациях.
Эти goroutines, использующие наши функциональные литералы, ссылаются на один и тот же v, и поэтому все они имеют гонку данных по этой переменная: программа, выполняющая lo oop , записывает в нее , а функции, выполняющие литералы функции read * 10 86 * из него - одновременно и без какой-либо синхронизации.

Надеюсь, к настоящему времени вы должны увидеть, как кусочки головоломки собираются вместе: в коде

    for _, v := range apps {
        v := v
        t.Run("", func(t *testing.T) {
            expectedOutput = `=` + v + `
            // ...

литерал функции передан t.Run закрывается за v, expectedOutput, cmpOpts.SingleApp (и может быть что-то еще), а затем t.Run() заставляет эту функцию буквально запускаться в отдельной процедуре, как задокументировано, - создавая гонку данных classi c на expectedOutput и cmpOpts.SingleApp и все остальное, кроме v (fre * 1121) * переменная на каждой итерации) или t (передается вызову функции literal).

Вы можете запустить go test -race -run=TestIntegrationAppsWithProductionSelf ./..., чтобы увидеть, как задействованный детектор гонки разбивает код вашего тестового примера.

0 голосов
/ 08 апреля 2020

Я собираюсь опубликовать то, что на самом деле сработало, но (если вопрос не закрыт), я приму ответ, который фактически уточняет его.

Проблема заключалась в том, что переменная использовалась для хранения expectedOutput было объявлено с объявлением внутри функции TestIntegrationAppsWithProductionSelf, но вне for l oop (теперь это отражено во фрагменте кода исходного вопроса).

Что сработало, так это удаление var expectedOutput string заявление и сделать в течение for l oop

    for _, v := range apps {
        v := v
        expectedOutput := `=` + v + `
`
        t.Run("", func(t *testing.T) {
            t.Parallel()
            cmpOpts.SingleApp = v
            t.Logf("\t\tTesting %s\n", v)
            buf, err := VarsCmp(output, cmpOpts)
            if err != nil {
                t.Fatalf("ERROR executing var comparison for %s: %s\n", v, err)
            }
            //assert.Equal(t, expectedOutput, buf.String())
            if expectedOutput != buf.String() {
                t.Errorf("Failed! Expected %s - Actual: %s\n", expectedOutput, buf.String())
            }
        })
    }

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...