[BACKPORT] add simulated state serialization between tofu test runs (#2274) (#2276)

Signed-off-by: ollevche <ollevche@gmail.com>
This commit is contained in:
Oleksandr Levchenkov 2024-12-10 17:23:56 +02:00 committed by GitHub
parent 82fdce1c7d
commit 8fd25f3346
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 90 additions and 0 deletions

View File

@ -1,5 +1,7 @@
## 1.8.8 (unreleased)
BUG FIXES:
* `tofu test` now removes outputs of destroyed modules between different test runs. ([#2274](https://github.com/opentofu/opentofu/pull/2274))
## 1.8.7

View File

@ -6,6 +6,7 @@
package command
import (
"bytes"
"context"
"fmt"
"log"
@ -28,6 +29,7 @@ import (
"github.com/opentofu/opentofu/internal/moduletest"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/states/statefile"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/opentofu/opentofu/internal/tofu"
)
@ -455,6 +457,26 @@ func (runner *TestFileRunner) ExecuteTestFile(file *moduletest.File) {
state, updatedState := runner.ExecuteTestRun(run, file, runner.States[key].State, config)
if updatedState {
var err error
// We need to simulate state serialization between multiple runs
// due to its side effects. One of such side effects is removal
// of destroyed non-root module outputs. This is not handled
// during graph walk since those values are not stored in the
// state file. This is more of a weird workaround instead of a
// proper fix, unfortunately.
state, err = simulateStateSerialization(state)
if err != nil {
run.Diagnostics = run.Diagnostics.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failure during state serialization",
Detail: err.Error(),
})
// We cannot reuse state later so that's a hard stop.
return
}
// Only update the most recent run and state if the state was
// actually updated by this change. We want to use the run that
// most recently updated the tracked state as the cleanup
@ -1261,3 +1283,24 @@ func (runner *TestFileRunner) prepareInputVariablesForAssertions(config *configs
config.Module.Variables = currentVars
}, diags
}
// simulateStateSerialization takes a state, serializes it, deserializes it
// and then returns. This is useful for state writing side effects without
// actually writing a state file.
func simulateStateSerialization(state *states.State) (*states.State, error) {
buff := &bytes.Buffer{}
f := statefile.New(state, "", 0)
err := statefile.Write(f, buff, encryption.StateEncryptionDisabled())
if err != nil {
return nil, fmt.Errorf("writing state to buffer: %w", err)
}
f, err = statefile.Read(buff, encryption.StateEncryptionDisabled())
if err != nil {
return nil, fmt.Errorf("reading state from buffer: %w", err)
}
return f.State, nil
}

View File

@ -820,6 +820,10 @@ func TestTest_Modules(t *testing.T) {
expected: "main.tftest.hcl... pass\n run \"setup\"... pass\n run \"test\"... pass\n\nSuccess! 2 passed, 0 failed.\n",
code: 0,
},
"destroyed_mod_outputs": {
expected: "main.tftest.hcl... pass\n run \"first_apply\"... pass\n run \"second_apply\"... pass\n\nSuccess! 2 passed, 0 failed.\n",
code: 0,
},
}
for name, tc := range tcs {

View File

@ -0,0 +1,9 @@
variable "numbers" {
type = set(string)
}
module "mod" {
source = "./mod"
for_each = var.numbers
val = each.key
}

View File

@ -0,0 +1,21 @@
run "first_apply" {
variables {
numbers = [ "a", "b" ]
}
assert {
condition = length(module.mod) == 2
error_message = "Amount of module outputs is wrong"
}
}
run "second_apply" {
variables {
numbers = [ "c", "d" ]
}
assert {
condition = length(module.mod) == 2
error_message = "Amount of module outputs is wrong (persisted outputs?)"
}
}

View File

@ -0,0 +1,11 @@
variable "val" {
}
output "val" {
value = "${var.val}_${test_resource.resource.id}"
}
resource "test_resource" "resource" {
id = "598318e0"
value = var.val
}