We had some strange issues with a test that was failing with serialization errors once in a while. It would fail only when multiple tests where run, when run stand-alone it would never fail.
I spend some time hunting this down, and it turns out to ultimately be the result of JsConfig.Reset()
not always resetting static state consistently. (We are stopping and starting AppHost, and the apphost Dispose()
calls JsConfig.Reset()
).
What happens is that JsConfig.Reset()
loops through types it has seen to reset the config for each of these types in the order it has seen them. The reset triggers JsonReader<T>.Refresh()
to recreate itās ReadFn
. The first problem is that this happens while the setup for the other types are not all reset yet, so it may still pick up some customization for underlying properties which are not reset yet. The first problem is that it creates the ReadFn
instead of just clearing it, which means later customization to underlying properties is not picked up.
All this being static it persist over multiple test in a test run which means that depending on test order a test may leave a ReadFn
in an inconsistent state after it disposed an AppHost.
The following test demonstrates this behavior. It has two tests, which are expected to succeed, but fail. It also has a test which succeeds when run individually but fails when all tests are run.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using ServiceStack;
using ServiceStack.Text;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace JsConfigTests
{
public class SomeObject
{
public CustomField Value { get; set; } = new CustomField(new byte[0]);
}
public class CustomField
{
private readonly String val;
public String Value
{
get { return val; }
}
public CustomField(byte[] val)
{
this.val = val.FromUtf8Bytes();
}
}
[TestCaseOrderer("JsConfigTests.AlphabeticalOrderer", "JsConfigTests")]
public class JsConfigTest
{
public static void SetupSerialization()
{
JsConfig.ThrowOnError = true;
JsConfig<CustomField>.DeSerializeFn = x => new CustomField(Encoding.UTF8.GetBytes(x));
}
// This test should succeed, but doesn't because JsConfig.Reset() leaves an inconsistent state.
// It breaks because the Reset() created a new ReadFn for SomeObject while the custom serializer for
// CustomField was still in place.
[Fact]
public void B_FailAfterReset()
{
// Test Json
var str = "{\"value\":\"Test\"}";
// Configure serialization
SetupSerialization();
// Prove we can read the thing correctly
SomeObject obj;
obj = str.FromJson<SomeObject>();
Assert.NotNull(obj);
Assert.Equal("Test", obj.Value.Value);
// Dispose the AppHost, this will clear custom serialization functions and call JsConfig<T>.Refresh()
JsConfig.Reset();
JsConfig.ThrowOnError = true;
// Because custom serialization is cleared we expect serialization to now fail.
Assert.Throws<SerializationException>(() => str.FromJson<SomeObject>());
}
// In this case the ReadFn for SomeObject is reset correctly due to different ordering. However, because a new
// ReadFn is created, instead of it just being cleared, subsequent custom serialization setup is not picked up,
// because that only resets the ReadFn for CustomField, not the one for SomeObject.
[Fact]
public void A_FailAfterSecondSetup()
{
// Test Json
var str = "{\"value\":\"Test\"}";
// Configure serialization
SetupSerialization();
// Make sure to hit CustomField first, this ensures it's also reset first.
var field = "LALA".FromJson<CustomField>();
Assert.NotNull(field);
Assert.Equal("LALA", field.Value);
// Prove we can read the thing correctly
SomeObject obj;
obj = str.FromJson<SomeObject>();
Assert.NotNull(obj);
Assert.Equal("Test", obj.Value.Value);
// Dispose the AppHost, this will clear custom serialization functions and call JsConfig<T>.Refresh()
JsConfig.Reset();
JsConfig.ThrowOnError = true;
// Because custom serialization is cleared we expect
Assert.Throws<SerializationException>(() => str.FromJson<SomeObject>());
// Now we set the custom deserialize function again, expecting serialization to work again.
SetupSerialization();
// But it doesn't work. Setting the deserialization function refreshes the config for Custom, but
// the config for Thing still has the default function.
obj = str.FromJson<SomeObject>();
Assert.NotNull(obj);
Assert.Equal("Test", obj.Value.Value);
}
// This is the most basic test, it runs fine when run stand-alone. But when running multiple tests it fails
// when a previous test leaves an incorrect ReadFn for SomeObject.
[Fact]
public void C_Succeed()
{
// Test Json
var str = "{\"value\":\"Test\"}";
// Configure serialization
SetupSerialization();
// Prove we can read the thing correctly
SomeObject obj;
obj = str.FromJson<SomeObject>();
Assert.NotNull(obj);
Assert.Equal("Test", obj.Value.Value);
}
}
public class AlphabeticalOrderer : ITestCaseOrderer
{
public IEnumerable<TTestCase> OrderTestCases<TTestCase>(IEnumerable<TTestCase> testCases) where TTestCase : ITestCase
{
return testCases.OrderBy(x => x.DisplayName);
}
}
}
Solving this may not be trivial, because the statics canāt be deleted to completely reset things. I think somehow invalidating ReadFn
in JsonReader<T>
instead of recreating it would solve the error scenarioās Iāve seen, but Iām not sure if that would create new issues.
P.s. At least for us this only affects testing, not production, and Iāve got a workaround for that already. So itās not really urgent for us.