Files
quantconnect--lean/Tests/Common/Storage/LocalObjectStoreTests.cs
Colton Sellers 76a53eb096 Local Object Store Refactor and Fixes (#4880)
* Store temp files in subdirectory

* Fix Dispose case for new temp dir

* Adjust tests for new temp dir

* Dispose unit tests

* Unit test for issue 4811

* Refactor for not using temp files

* Fix storage checks for saving data, plus tests

* Use Base64 for storing keys and decoding them; handles odd key strings

* Don't allow "?" in a key

* Address review

* Deleted test cases

* PersistData handle deletion of files

* Refactor GetFilePath to use Persist()

* Make PathForKey protected
2020-10-30 21:36:23 -03:00

443 lines
16 KiB
C#

/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using QuantConnect.Configuration;
using QuantConnect.Lean.Engine.Storage;
using QuantConnect.Packets;
using QuantConnect.Research;
using QuantConnect.Storage;
using QuantConnect.Util;
namespace QuantConnect.Tests.Common.Storage
{
[TestFixture]
public class LocalObjectStoreTests
{
private static readonly string TestStorageRoot = $"./{nameof(LocalObjectStoreTests)}";
private static readonly string StorageRootConfigurationValue = Config.Get("object-store-root");
private ObjectStore _store;
[OneTimeSetUp]
public void Setup()
{
Config.Set("object-store-root", TestStorageRoot);
_store = new ObjectStore(new LocalObjectStore());
_store.Initialize("CSharp-TestAlgorithm", 0, 0, "", new Controls());
}
[OneTimeTearDown]
public void Cleanup()
{
_store.DisposeSafely();
Config.Set("object-store-root", StorageRootConfigurationValue);
try
{
Directory.Delete(TestStorageRoot, true);
}
catch
{
}
Config.Reset();
}
[TestCase(FileAccess.Read, false)]
[TestCase(FileAccess.ReadWrite, false)]
[TestCase(0, true)]
[TestCase(FileAccess.Write, true)]
public void GetFilePathPermissions(FileAccess permissions, bool shouldThrow)
{
var store = new TestLocalObjectStore();
store.Initialize($"CSharp-TestAlgorithm-{permissions}", 0, 0, "", new Controls { StoragePermissions = permissions });
if (shouldThrow)
{
Assert.Throws<InvalidOperationException>(() => store.GetFilePath("Jose"));
}
else
{
Assert.Throws<KeyNotFoundException>(() => store.GetFilePath("Jose"));
}
}
[TestCase(FileAccess.Read, false)]
[TestCase(FileAccess.ReadWrite, false)]
[TestCase(0, true)]
[TestCase(FileAccess.Write, true)]
public void ReadBytesPermissions(FileAccess permissions, bool shouldThrow)
{
var store = new TestLocalObjectStore();
store.Initialize($"CSharp-TestAlgorithm-{permissions}", 0, 0, "", new Controls { StoragePermissions = permissions });
if (shouldThrow)
{
Assert.Throws<InvalidOperationException>(() => store.ReadBytes("Jose"));
}
else
{
Assert.Throws<KeyNotFoundException>(() => store.ReadBytes("Jose"));
}
}
[TestCase(FileAccess.Read, true)]
[TestCase(FileAccess.ReadWrite, false)]
[TestCase(0, true)]
[TestCase(FileAccess.Write, false)]
public void SaveBytesPermissions(FileAccess permissions, bool shouldThrow)
{
var store = new TestLocalObjectStore();
store.Initialize($"CSharp-TestAlgorithm-{permissions}", 0, 0, "", new Controls { StoragePermissions = permissions });
if (shouldThrow)
{
Assert.Throws<InvalidOperationException>(() => store.SaveBytes("Jose", new byte[] { 0 }));
}
else
{
Assert.IsTrue(store.SaveBytes("Jose", new byte[] { 0 }));
}
}
[TestCase(FileAccess.Read, true)]
[TestCase(FileAccess.ReadWrite, false)]
[TestCase(0, true)]
[TestCase(FileAccess.Write, false)]
public void DeletePermissions(FileAccess permissions, bool shouldThrow)
{
var store = new TestLocalObjectStore();
store.Initialize($"CSharp-TestAlgorithm-{permissions}", 0, 0, "", new Controls { StoragePermissions = permissions });
if (shouldThrow)
{
Assert.Throws<InvalidOperationException>(() => store.Delete("Jose"));
}
else
{
Assert.IsFalse(store.Delete("Jose"));
}
}
[Test]
public void GetFilePathAndDelete()
{
using (var store = new ObjectStore(new LocalObjectStore()))
{
store.Initialize("test", 0, 0, "", new Controls() { PersistenceIntervalSeconds = -1 });
Assert.IsTrue(Directory.Exists("./LocalObjectStoreTests/test"));
var key = "ILove";
store.SaveString(key, "Pizza");
var path = store.GetFilePath(key);
Assert.IsTrue(File.Exists(path));
store.Delete(key);
Assert.IsFalse(File.Exists(path));
}
}
[TestCase(FileAccess.Read, false)]
[TestCase(FileAccess.ReadWrite, false)]
[TestCase(0, true)]
[TestCase(FileAccess.Write, true)]
public void ContainsKeyPermissions(FileAccess permissions, bool shouldThrow)
{
var store = new TestLocalObjectStore();
store.Initialize($"CSharp-TestAlgorithm-{permissions}", 0, 0, "", new Controls {StoragePermissions = permissions});
if (shouldThrow)
{
Assert.Throws<InvalidOperationException>(() => store.ContainsKey("Jose"));
}
else
{
Assert.IsFalse(store.ContainsKey("Jose"));
}
}
[TestCase(FileAccess.Read, false)]
[TestCase(FileAccess.ReadWrite, false)]
[TestCase(0, true)]
[TestCase(FileAccess.Write, true)]
public void InitializationPermissions(FileAccess permissions, bool shouldThrow)
{
var store = new TestLocalObjectStore();
var dir = Path.Combine(TestStorageRoot, $"CSharp-TestAlgorithm-8");
Directory.CreateDirectory(dir);
//Determine filename for key "Jose" using Base64
var filename = Convert.ToBase64String(Encoding.UTF8.GetBytes("Jose"));
File.WriteAllText(Path.Combine(dir, filename), "Pepe");
store.Initialize($"CSharp-TestAlgorithm-8", 0, 0, "", new Controls { StoragePermissions = permissions });
if (shouldThrow)
{
Assert.Throws<InvalidOperationException>(() => store.ContainsKey("Jose"));
}
else
{
Assert.IsTrue(store.ContainsKey("Jose"));
}
Directory.Delete(dir, true);
}
[Test]
public void PersistCalledSynchronously()
{
var store = new TestLocalObjectStore();
store.Initialize("CSharp-TestAlgorithm2", 0, 0, "", new Controls
{
PersistenceIntervalSeconds = -1
});
store.SaveBytes("Pepe", new byte[] {1});
Assert.AreEqual(1, store.ReadBytes("Pepe").Single());
Assert.IsTrue(store.PersistDataCalled);
store.PersistDataCalled = false;
store.Delete("Pepe");
Assert.IsTrue(store.PersistDataCalled);
Assert.IsFalse(store.ContainsKey("Pepe"));
store.DisposeSafely();
}
[Test]
public void ThrowsKeyNotFoundException_WhenObjectStoreDoesNotContainKey()
{
var error = Assert.Throws<KeyNotFoundException>(
() => _store.ReadBytes("missing.missing")
);
Assert.IsTrue(error.Message.Contains("Please use ObjectStore.ContainsKey(key)"));
}
[TestCase("my_key", "./LocalObjectStoreTests/CSharp-TestAlgorithm/bXlfa2V5")]
[TestCase("test/123", "./LocalObjectStoreTests/CSharp-TestAlgorithm/dGVzdC8xMjM=")]
[TestCase("**abc**", "./LocalObjectStoreTests/CSharp-TestAlgorithm/KiphYmMqKg==")]
[TestCase("<random>", "./LocalObjectStoreTests/CSharp-TestAlgorithm/PHJhbmRvbT4=")]
[TestCase("|", "./LocalObjectStoreTests/CSharp-TestAlgorithm/fA==")]
public void GetFilePathReturnsFileName(string key, string expectedRelativePath)
{
var expectedPath = Path.GetFullPath(expectedRelativePath).Replace("\\", "/");
_store.SaveString(key, "data");
Assert.AreEqual(expectedPath, _store.GetFilePath(key).Replace("\\", "/"));
}
[Test]
public void SavesAndLoadsText()
{
const string expectedText = "12;26";
Assert.IsTrue(_store.SaveString("my_settings_text", expectedText));
var actualText = _store.Read("my_settings_text");
Assert.AreEqual(expectedText, actualText);
}
[Test]
public void SizeLimitIsRespected()
{
{
var validData = new byte[1024 * 1024 * 4];
Assert.IsTrue(_store.SaveBytes("my_settings_text", validData));
}
{
var invalidData = new byte[1024 * 1024 * 6];
Assert.IsFalse(_store.SaveBytes("my_settings_text", invalidData));
}
_store.Delete("my_settings_text");
}
[Test]
public void SavesAndLoadsJson()
{
var expected = new TestSettings { EmaFastPeriod = 12, EmaSlowPeriod = 26 };
Assert.IsTrue(_store.SaveJson("my_settings_json", expected));
var actual = _store.ReadJson<TestSettings>("my_settings_json");
Assert.AreEqual(expected.EmaFastPeriod, actual.EmaFastPeriod);
Assert.AreEqual(expected.EmaSlowPeriod, actual.EmaSlowPeriod);
}
[Test]
public void SavesAndLoadsXml()
{
var expected = new TestSettings { EmaFastPeriod = 12, EmaSlowPeriod = 26 };
Assert.IsTrue(_store.SaveXml("my_settings_xml", expected));
var actual = _store.ReadXml<TestSettings>("my_settings_xml");
Assert.AreEqual(expected.EmaFastPeriod, actual.EmaFastPeriod);
Assert.AreEqual(expected.EmaSlowPeriod, actual.EmaSlowPeriod);
}
[Test]
public void ThrowsIfKeyIsNull()
{
Assert.Throws<ArgumentNullException>(() => _store.ContainsKey(null));
Assert.Throws<ArgumentNullException>(() => _store.ReadBytes(null));
Assert.Throws<ArgumentNullException>(() => _store.SaveBytes(null, null));
Assert.Throws<ArgumentNullException>(() => _store.Delete(null));
Assert.Throws<ArgumentNullException>(() => _store.GetFilePath(null));
}
[Test]
public void DisposeRemovesEmptyStorageFolder()
{
using (var store = new LocalObjectStore())
{
store.Initialize("unused", 0, 0, "", new Controls());
Assert.IsTrue(Directory.Exists("./LocalObjectStoreTests/unused"));
}
Assert.IsFalse(Directory.Exists("./LocalObjectStoreTests/unused"));
}
[Test]
public void DisposeDoesNotDeleteStoreFiles()
{
string path;
using (var store = new LocalObjectStore())
{
store.Initialize("test", 0, 0, "", new Controls() {PersistenceIntervalSeconds = -1});
Assert.IsTrue(Directory.Exists("./LocalObjectStoreTests/test"));
var validData = new byte[1024 * 1024 * 4];
var saved = store.SaveBytes("a.txt", validData);
Assert.IsTrue(saved);
path = store.GetFilePath("a.txt");
Assert.IsTrue(File.Exists(path));
}
// Check that it still exists
Assert.IsTrue(File.Exists(path));
}
[Test]
public void QuantBookObjectStoreBehavior()
{
// Test for issue #4811, on loop store objects would duplicate
for (int i = 0; i < 3; i++)
{
// Create a QuantBook and save some data
var qb = new QuantBook();
qb.ObjectStore.Save("a.txt", "1010101010101010101010");
Assert.IsTrue(qb.ObjectStore.ContainsKey("a.txt"));
// Assert the store has only a.txt
var store = qb.ObjectStore.GetEnumerator().AsEnumerable().ToList();
Assert.IsTrue(store.Count == 1);
Assert.IsTrue(store[0].Key == "a.txt");
// Get the file path and verify it exists
var path = qb.ObjectStore.GetFilePath("a.txt");
Assert.IsTrue(File.Exists(path));
}
}
[Test]
public void OversizedObject()
{
// Create a big byte array
var bytesToWrite = new byte[7000000];
// Attempt to save it to local store with 5MB cap
Assert.IsFalse(_store.SaveBytes("test", bytesToWrite));
}
[Test]
public void TooManyObjects()
{
// Write 100 Files first, should not throw
for (int i = _store.Count(); i < 100; i++)
{
Assert.IsTrue(_store.SaveString($"{i}", $"{i}"));
}
// Write 1 more; should throw
Assert.IsFalse(_store.SaveString("breaker", "gotem"));
}
[Test]
public void DeletedObjectIsNotReloaded()
{
using (var store = new LocalObjectStore())
{
store.Initialize("test", 0, 0, "", new Controls());
Assert.IsTrue(Directory.Exists("./LocalObjectStoreTests/test"));
var validData = new byte[1024 * 4];
store.SaveBytes("a.txt", validData);
Assert.IsTrue(store.ContainsKey("a.txt"));
store.SaveBytes("b.txt", validData);
Assert.IsTrue(store.ContainsKey("b.txt"));
// Assert the store has our two objects
var storedObj = store.GetEnumerator().AsEnumerable().ToList();
Assert.IsTrue(storedObj.Count == 2);
// Delete a.txt and close this store down
store.Delete("a.txt");
Assert.IsFalse(store.ContainsKey("a.txt"));
}
using (var store = new LocalObjectStore())
{
// Check that the dir still exists, it had files so it shouldn't have deleted
Assert.IsTrue(Directory.Exists("./LocalObjectStoreTests/test"));
store.Initialize("test", 0, 0, "", new Controls());
// Check our files; a should be gone, b should be there
Assert.IsFalse(store.ContainsKey("a.txt"));
Assert.IsTrue(store.ContainsKey("b.txt"));
}
}
public class TestSettings
{
public int EmaFastPeriod { get; set; }
public int EmaSlowPeriod { get; set; }
}
private class TestLocalObjectStore : LocalObjectStore
{
public bool PersistDataCalled { get; set; }
protected override bool PersistData(IEnumerable<KeyValuePair<string, byte[]>> data)
{
PersistDataCalled = true;
return base.PersistData(data);
}
}
}
}