EndToEnd Tests finally pass!

This commit is contained in:
Timothy Baldridge 2020-03-28 12:22:53 -06:00
parent 129f6b4fc2
commit b605879d6a
13 changed files with 128 additions and 206 deletions

View File

@ -166,6 +166,8 @@ namespace Wabbajack.Common
}
}
public static AbsolutePath EntryPoint => ((AbsolutePath)Assembly.GetEntryAssembly().Location).Parent;
/// <summary>
/// Moves this file to the specified location
/// </summary>
@ -199,6 +201,7 @@ namespace Wabbajack.Common
/// <returns></returns>
public IEnumerable<AbsolutePath> EnumerateFiles(bool recursive = true)
{
if (!IsDirectory) return new AbsolutePath[0];
return Directory
.EnumerateFiles(_path, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
.Select(path => new AbsolutePath(path, true));
@ -274,11 +277,12 @@ namespace Wabbajack.Common
public AbsolutePath Combine(params RelativePath[] paths)
{
return new AbsolutePath(Path.Combine(paths.Select(s => (string)s).Cons(_path).ToArray()));
return new AbsolutePath(Path.Combine(paths.Select(s => (string)s).Where(s => s != null).Cons(_path).ToArray()));
}
public AbsolutePath Combine(params string[] paths)
{
return new AbsolutePath(Path.Combine(paths.Cons(_path).ToArray()));
}
@ -366,8 +370,17 @@ namespace Wabbajack.Common
public RelativePath(string path)
{
_path = path.ToLowerInvariant().Replace("/", "\\").Trim('\\');
Extension = new Extension(Path.GetExtension(path));
var trimmed = path.ToLowerInvariant().Replace("/", "\\").Trim('\\');
if (string.IsNullOrEmpty(trimmed))
{
_path = null;
Extension = default;
}
else
{
_path = trimmed;
Extension = new Extension(Path.GetExtension(path));
}
Validate();
}
@ -398,7 +411,7 @@ namespace Wabbajack.Common
public AbsolutePath RelativeTo(AbsolutePath abs)
{
return new AbsolutePath(Path.Combine((string)abs, _path));
return _path == null ? abs : new AbsolutePath(Path.Combine((string)abs, _path));
}
public AbsolutePath RelativeToEntryPoint()
@ -431,6 +444,20 @@ namespace Wabbajack.Common
public RelativePath FileName => new RelativePath(Path.GetFileName(_path));
public RelativePath FileNameWithoutExtension => (RelativePath)Path.GetFileNameWithoutExtension(_path);
public RelativePath TopParent
{
get
{
var curr = this;
while (curr.Parent != default)
curr = curr.Parent;
return curr;
}
}
public bool Equals(RelativePath other)
{
@ -528,6 +555,7 @@ namespace Wabbajack.Common
return new RelativePath(rdr.ReadString());
}
public static T[] Add<T>(this T[] arr, T itm)
{
var newArr = new T[arr.Length + 1];

View File

@ -93,10 +93,8 @@ namespace Wabbajack.Common
private async Task AddNewThreadsIfNeeded(int desired)
{
var started = DateTime.Now;
using (await _lock.Wait())
{
var end = DateTime.Now - started;
DesiredNumWorkers = desired;
while (DesiredNumWorkers > _tasks.Count)
{
@ -143,10 +141,8 @@ namespace Wabbajack.Common
if (DesiredNumWorkers >= _tasks.Count) continue;
// Noticed that we may need to shut down, lock and check again
var start = DateTime.Now;
using (await _lock.Wait())
{
var end = DateTime.Now - start;
// Check if another thread shut down before this one and got us back to the desired amount already
if (DesiredNumWorkers >= _tasks.Count) continue;

View File

@ -3,27 +3,30 @@ using System.Linq;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Newtonsoft.Json;
using Wabbajack.Common;
namespace Wabbajack.Lib.CompilationSteps
{
public class IgnoreOtherProfiles : ACompilationStep
{
private readonly IEnumerable<string> _profiles;
private readonly IEnumerable<AbsolutePath> _profiles;
private readonly MO2Compiler _mo2Compiler;
private AbsolutePath _modProfilesFolder;
public IgnoreOtherProfiles(ACompiler compiler) : base(compiler)
{
_mo2Compiler = (MO2Compiler) compiler;
_modProfilesFolder = _mo2Compiler.MO2Folder.Combine("profiles");
_profiles = _mo2Compiler.SelectedProfiles
.Select(p => Path.Combine("profiles", p) + "\\")
_profiles = _mo2Compiler.SelectedProfiles
.Select(p => _modProfilesFolder.Combine(p))
.ToList();
}
public override async ValueTask<Directive> Run(RawSourceFile source)
{
if (!source.Path.StartsWith("profiles\\")) return null;
if (_profiles.Any(profile => source.Path.StartsWith(profile))) return null;
if (!source.AbsolutePath.InFolder(_modProfilesFolder)) return null;
if (_profiles.Any(profile => source.AbsolutePath.InFolder(profile))) return null;
var c = source.EvolveTo<IgnoredDirectly>();
c.Reason = "File not for selected profiles";
return c;

View File

@ -14,10 +14,12 @@ namespace Wabbajack.Lib.CompilationSteps
private readonly Dictionary<RelativePath, IGrouping<RelativePath, VirtualFile>> _indexed;
private VirtualFile _bsa;
private Dictionary<RelativePath, VirtualFile> _indexedByName;
private MO2Compiler _mo2Compiler;
public IncludePatches(ACompiler compiler, VirtualFile constructingFromBSA = null) : base(compiler)
{
_bsa = constructingFromBSA;
_mo2Compiler = (MO2Compiler)compiler;
_indexed = _compiler.IndexedFiles.Values
.SelectMany(f => f)
.GroupBy(f => f.Name.FileName)
@ -39,16 +41,19 @@ namespace Wabbajack.Lib.CompilationSteps
if (!_indexed.TryGetValue(name, out var choices))
_indexed.TryGetValue(nameWithoutExt, out choices);
dynamic mod_ini;
if (_bsa == null)
mod_ini = ((MO2Compiler)_compiler).ModMetas.FirstOrDefault(f => source.Path.StartsWith(f.Key)).Value;
else
dynamic modIni = null;
if (source.AbsolutePath.InFolder(_mo2Compiler.MO2ModsFolder))
{
var bsa_path = _bsa.FullPath.Paths.Last().RelativeTo(((MO2Compiler)_compiler).MO2Folder);
mod_ini = ((MO2Compiler)_compiler).ModMetas.FirstOrDefault(f => ((string)bsa_path).StartsWith((string)f.Key)).Value;
if (_bsa == null)
((MO2Compiler)_compiler).ModInis.TryGetValue(ModForFile(source.AbsolutePath), out modIni);
else
{
var bsaPath = _bsa.FullPath.Paths.Last().RelativeTo(((MO2Compiler)_compiler).MO2Folder);
((MO2Compiler)_compiler).ModInis.TryGetValue(ModForFile(bsaPath), out modIni);
}
}
var installationFile = mod_ini?.General?.installationFile;
var installationFile = modIni?.General?.installationFile;
VirtualFile found = null;
@ -68,7 +73,7 @@ namespace Wabbajack.Lib.CompilationSteps
}
// Find based on matchAll=<archivename> in [General] in meta.ini
var matchAllName = (RelativePath?)mod_ini?.General?.matchAll;
var matchAllName = (RelativePath?)modIni?.General?.matchAll;
if (matchAllName != null)
{
if (_indexedByName.TryGetValue(matchAllName.Value, out var arch))
@ -97,6 +102,12 @@ namespace Wabbajack.Lib.CompilationSteps
return e;
}
private AbsolutePath ModForFile(AbsolutePath file)
{
return file.RelativeTo(((MO2Compiler)_compiler).MO2ModsFolder).TopParent
.RelativeTo(((MO2Compiler)_compiler).MO2ModsFolder);
}
public override IState GetState()
{
return new State();

View File

@ -10,18 +10,18 @@ namespace Wabbajack.Lib.CompilationSteps
{
public class IncludeThisProfile : ACompilationStep
{
private readonly IEnumerable<string> _correctProfiles;
private readonly IEnumerable<AbsolutePath> _correctProfiles;
private readonly MO2Compiler _mo2Compiler;
public IncludeThisProfile(ACompiler compiler) : base(compiler)
{
_mo2Compiler = (MO2Compiler) compiler;
_correctProfiles = _mo2Compiler.SelectedProfiles.Select(p => Path.Combine("profiles", p) + "\\").ToList();
_correctProfiles = _mo2Compiler.SelectedProfiles.Select(p => _mo2Compiler.MO2ProfileDir.Parent.Combine(p)).ToList();
}
public override async ValueTask<Directive> Run(RawSourceFile source)
{
if (_correctProfiles.Any(p => source.Path.StartsWith(p)))
if (_correctProfiles.Any(p => source.AbsolutePath.InFolder(p)))
{
var data = source.Path.FileName == Consts.ModListTxt
? await ReadAndCleanModlist(source.AbsolutePath)

View File

@ -33,7 +33,6 @@ namespace Wabbajack.Lib
public AbsolutePath MO2ModsFolder => MO2Folder.Combine(Consts.MO2ModFolderName);
public string MO2Profile { get; }
public Dictionary<RelativePath, dynamic> ModMetas { get; set; }
public override ModManager ModManager => ModManager.MO2;
@ -211,13 +210,6 @@ namespace Wabbajack.Lib
}
ModMetas = MO2Folder.Combine(Consts.MO2ModFolderName).EnumerateFiles()
.Keep(f =>
{
var path = f.Combine("meta.ini");
return path.Exists ? (f, path.LoadIniFile()) : default;
}).ToDictionary(f => f.f.RelativeTo(MO2Folder), v => v.Item2);
IndexedFiles = IndexedArchives.SelectMany(f => f.File.ThisAndAllChildren)
.OrderBy(f => f.NestingFactor)
.GroupBy(f => f.Hash)
@ -258,11 +250,9 @@ namespace Wabbajack.Lib
{
var modName = f.FileName;
var metaPath = f.Combine("meta.ini");
if (metaPath.Exists)
return (mod_name: f, metaPath.LoadIniFile());
return default;
return metaPath.Exists ? (mod_name: f, metaPath.LoadIniFile()) : default;
})
.Where(f => f.Item2 != null)
.Where(f => f.Item1 != default)
.ToDictionary(f => f.Item1, f => f.Item2);
if (cancel.IsCancellationRequested) return false;

View File

@ -69,7 +69,7 @@ namespace Wabbajack.Test
await installer.Begin();
}
protected SystemParameters CreateDummySystemParameters()
public static SystemParameters CreateDummySystemParameters()
{
return new SystemParameters
{

View File

@ -3,29 +3,29 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.NexusApi;
using Wabbajack.VirtualFileSystem;
using Xunit;
using Xunit.Abstractions;
namespace Wabbajack.Test
{
[TestClass]
public class EndToEndTests
public class EndToEndTests : IDisposable
{
private const string DOWNLOAD_FOLDER = "downloads";
private AbsolutePath _downloadFolder = "downloads".RelativeTo(AbsolutePath.EntryPoint);
private TestUtils utils = new TestUtils();
public TestContext TestContext { get; set; }
public ITestOutputHelper TestContext { get; set; }
public WorkQueue Queue { get; set; }
[TestInitialize]
public void TestInitialize()
public EndToEndTests(ITestOutputHelper helper)
{
TestContext = helper;
Queue = new WorkQueue();
Consts.TestMode = true;
@ -33,18 +33,16 @@ namespace Wabbajack.Test
utils.Game = Game.SkyrimSpecialEdition;
Utils.LogMessages.Subscribe(f => TestContext.WriteLine($"{DateTime.Now} - {f}"));
if (!Directory.Exists(DOWNLOAD_FOLDER))
Directory.CreateDirectory(DOWNLOAD_FOLDER);
_downloadFolder.CreateDirectory();
}
[TestCleanup]
public void Cleanup()
public void Dispose()
{
Queue.Dispose();
}
[TestMethod]
[Fact]
public async Task CreateModlist()
{
var profile = utils.AddProfile("Default");
@ -52,14 +50,11 @@ namespace Wabbajack.Test
await DownloadAndInstall(
"https://github.com/ModOrganizer2/modorganizer/releases/download/v2.2.1/Mod.Organizer.2.2.1.7z",
"Mod.Organizer.2.2.1.7z",
utils.MO2Folder);
File.WriteAllLines(Path.Combine(utils.DownloadsFolder, "Mod.Organizer.2.2.1.7z.meta"),
new List<string>
{
"Mod.Organizer.2.2.1.7z");
await utils.DownloadsFolder.Combine("Mod.Organizer.2.2.1.7z.meta").WriteAllLinesAsync(
"[General]",
"directURL=https://github.com/ModOrganizer2/modorganizer/releases/download/v2.2.1/Mod.Organizer.2.2.1.7z"
});
);
var modfiles = await Task.WhenAll(
DownloadAndInstall(Game.SkyrimSpecialEdition, 12604, "SkyUI"),
@ -68,95 +63,85 @@ namespace Wabbajack.Test
DownloadAndInstall(Game.SkyrimSpecialEdition, 32359, "Frost Armor HDT"));
// We're going to fully patch this mod from another source.
File.Delete(modfiles[3].Download);
modfiles[3].Download.Delete();
utils.Configure();
await utils.Configure();
File.WriteAllLines(Path.Combine(modfiles[3].ModFolder, "meta.ini"), new []
{
await modfiles[3].ModFolder.Combine("meta.ini").WriteAllLinesAsync(
"[General]",
$"matchAll= {Path.GetFileName(modfiles[2].Download)}"
});
$"matchAll= {modfiles[2].Download.FileName}"
);
File.WriteAllLines(Path.Combine(utils.MO2Folder, "startup.bat"), new []
{
await utils.MO2Folder.Combine("startup.bat").WriteAllLinesAsync(
"ModOrganizer2.exe SKSE"
});
);
var modlist = await CompileAndInstall(profile);
await CompileAndInstall(profile);
utils.VerifyAllFiles();
var loot_folder = Path.Combine(utils.InstallFolder, "LOOT Config Files");
if (Directory.Exists(loot_folder))
Utils.DeleteDirectory(loot_folder);
await utils.InstallFolder.Combine(Consts.LOOTFolderFilesDir).DeleteDirectory();
var compiler = new MO2Compiler(
mo2Folder: utils.InstallFolder,
mo2Profile: profile,
outputFile: profile + Consts.ModListExtension);
compiler.MO2DownloadsFolder = Path.Combine(utils.DownloadsFolder);
Assert.IsTrue(await compiler.Begin());
outputFile: profile.RelativeTo(AbsolutePath.EntryPoint).WithExtension(Consts.ModListExtension));
compiler.MO2DownloadsFolder = utils.DownloadsFolder;
Assert.True(await compiler.Begin());
}
private async Task DownloadAndInstall(string url, string filename, string mod_name = null)
private async Task DownloadAndInstall(string url, string filename, string modName = null)
{
var src = Path.Combine(DOWNLOAD_FOLDER, filename);
if (!File.Exists(src))
var src = _downloadFolder.Combine(filename);
if (!src.Exists)
{
var state = DownloadDispatcher.ResolveArchive(url);
await state.Download(new Archive { Name = "Unknown"}, src);
}
if (!Directory.Exists(utils.DownloadsFolder))
{
Directory.CreateDirectory(utils.DownloadsFolder);
}
utils.DownloadsFolder.CreateDirectory();
await Utils.CopyFileAsync(src, Path.Combine(utils.DownloadsFolder, filename));
await src.CopyToAsync(utils.DownloadsFolder.Combine(filename));
await FileExtractor.ExtractAll(Queue, src,
mod_name == null ? utils.MO2Folder : Path.Combine(utils.ModsFolder, mod_name));
modName == null ? utils.MO2Folder : utils.ModsFolder.Combine(modName));
}
private async Task<(string Download, string ModFolder)> DownloadAndInstall(Game game, int modid, string mod_name)
private async Task<(AbsolutePath Download, AbsolutePath ModFolder)> DownloadAndInstall(Game game, int modId, string modName)
{
utils.AddMod(mod_name);
utils.AddMod(modName);
var client = await NexusApiClient.Get();
var resp = await client.GetModFiles(game, modid);
var resp = await client.GetModFiles(game, modId);
var file = resp.files.FirstOrDefault(f => f.is_primary) ?? resp.files.FirstOrDefault(f => !string.IsNullOrEmpty(f.category_name));
var src = Path.Combine(DOWNLOAD_FOLDER, file.file_name);
var src = _downloadFolder.Combine(file.file_name);
var ini = string.Join("\n",
new List<string>
{
"[General]",
$"gameName={game.MetaData().MO2ArchiveName}",
$"modID={modid}",
$"modID={modId}",
$"fileID={file.file_id}"
});
if (!File.Exists(src))
if (!src.Exists)
{
var state = (AbstractDownloadState)await DownloadDispatcher.ResolveArchive(ini.LoadIniString());
await state.Download(src);
}
utils.DownloadsFolder.CreateDirectory();
if (!Directory.Exists(utils.DownloadsFolder))
{
Directory.CreateDirectory(utils.DownloadsFolder);
}
var dest = utils.DownloadsFolder.Combine(file.file_name);
await src.CopyToAsync(dest);
var dest = Path.Combine(utils.DownloadsFolder, file.file_name);
await Utils.CopyFileAsync(src, dest);
var modFolder = Path.Combine(utils.ModsFolder, mod_name);
var modFolder = utils.ModsFolder.Combine(modName);
await FileExtractor.ExtractAll(Queue, src, modFolder);
File.WriteAllText(dest + Consts.MetaFileExtension, ini);
await dest.WithExtension(Consts.MetaFileExtension).WriteAllTextAsync(ini);
return (dest, modFolder);
}
@ -175,7 +160,7 @@ namespace Wabbajack.Test
modList: modlist,
outputFolder: utils.InstallFolder,
downloadFolder: utils.DownloadsFolder,
parameters: SystemParametersConstructor.Create());
parameters: ACompilerTest.CreateDummySystemParameters());
installer.GameFolder = utils.GameFolder;
await installer.Begin();
}
@ -185,8 +170,8 @@ namespace Wabbajack.Test
var compiler = new MO2Compiler(
mo2Folder: utils.MO2Folder,
mo2Profile: profile,
outputFile: profile + Consts.ModListExtension);
Assert.IsTrue(await compiler.Begin());
outputFile: profile.RelativeTo(AbsolutePath.EntryPoint).WithExtension(Consts.ModListExtension));
Assert.True(await compiler.Begin());
return compiler;
}
}

View File

@ -1,30 +0,0 @@
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
namespace Wabbajack.Test
{
public static class Extensions
{
public static void AssertIsFalse(this bool? condition)
{
Assert.IsFalse(condition ?? true, string.Empty, (object[])null);
}
public static void AssertIsTrue(this bool? condition)
{
Assert.IsTrue(condition ?? false, string.Empty, (object[])null);
}
public static async Task<T> RoundTripState<T>(this T state) where T : AbstractDownloadState
{
var ini = string.Join("\r\n", state.GetMetaIni()).LoadIniString();
var round = (AbstractDownloadState) await DownloadDispatcher.ResolveArchive(ini);
Assert.IsInstanceOfType(round, state.GetType());
Assert.AreEqual(state.PrimaryKeyString, round.PrimaryKeyString);
CollectionAssert.AreEqual(state.GetMetaIni(), round.GetMetaIni());
return (T)round;
}
}
}

View File

@ -1,17 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Reactive.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib.Downloaders;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Wabbajack.Test
{
@ -54,34 +53,30 @@ namespace Wabbajack.Test
}
}
[TestClass]
public class RestartingDownloadsTests
{
public TestContext TestContext { get; set; }
private ITestOutputHelper TestContext { get; set; }
[TestInitialize]
public void Setup()
public RestartingDownloadsTests(ITestOutputHelper helper)
{
TestContext = helper;
Utils.LogMessages.OfType<IInfo>().Subscribe(onNext: msg => TestContext.WriteLine(msg.ShortDescription));
Utils.LogMessages.OfType<IUserIntervention>().Subscribe(msg =>
TestContext.WriteLine("ERROR: User intervention required: " + msg.ShortDescription));
}
[TestMethod]
[Fact]
public async Task DownloadResume()
{
using (var server = new CrappyRandomServer())
{
var downloader = DownloadDispatcher.GetInstance<HTTPDownloader>();
var state = new HTTPDownloader.State {Url = $"http://localhost:{server.Port}/foo"};
using var testFile = new TempFile();
using var server = new CrappyRandomServer();
var state = new HTTPDownloader.State {Url = $"http://localhost:{server.Port}/foo"};
await state.Download("test.resume_file");
await state.Download(testFile.Path);
CollectionAssert.AreEqual(server.Data, File.ReadAllBytes("test.resume_file"));
Assert.Equal(server.Data, await testFile.Path.ReadAllBytesAsync());
if (File.Exists("test.resume_file"))
File.Delete("test.resume_file");
}
testFile.Path.Delete();
}
}

View File

@ -30,22 +30,11 @@ namespace Wabbajack.Test
{
get { return _port; }
}
/// <summary>
/// Construct server with given port.
/// </summary>
/// <param name="path">Directory path to serve.</param>
/// <param name="port">Port of the server.</param>
public SimpleHTTPServer(string path, int port)
{
this.Initialize(path, port);
}
/// <summary>
/// Construct server with suitable port.
/// </summary>
/// <param name="path">Directory path to serve.</param>
public SimpleHTTPServer(string path)
protected SimpleHTTPServer(string path)
{
//get an empty port
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
@ -76,9 +65,9 @@ namespace Wabbajack.Test
HttpListenerContext context = _listener.GetContext();
Process(context);
}
catch (Exception ex)
catch (Exception)
{
// ignored
}
}
}

View File

@ -14,18 +14,9 @@
<None Remove="Readme.md" />
<Compile Remove="AVortexCompilerTest.cs" />
<None Include="AVortexCompilerTest.cs" />
<Compile Remove="EndToEndTests.cs" />
<None Include="EndToEndTests.cs" />
<Compile Remove="Extensions.cs" />
<None Include="Extensions.cs" />
<Compile Remove="RestartingDownloadsTests.cs" />
<None Include="RestartingDownloadsTests.cs" />
<Compile Remove="SimpleHTTPServer.cs" />
<None Include="SimpleHTTPServer.cs" />
<Compile Remove="VortexTests.cs" />
<None Include="VortexTests.cs" />
<Compile Remove="WebAutomationTests.cs" />
<None Include="WebAutomationTests.cs" />
<Compile Remove="ZEditIntegrationTests.cs" />
<None Include="ZEditIntegrationTests.cs" />
<Compile Remove="CompilationStackTests.cs" />

View File

@ -1,36 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Lib.WebAutomation;
namespace Wabbajack.Test
{
/// <summary>
/// Summary description for WebAutomationTests
/// </summary>
[TestClass]
public class WebAutomationTests
{
/*
[TestMethod]
public async Task TestBasicNavigation()
{
using (var w = await Driver.Create())
{
await w.NavigateTo(new Uri("http://www.google.com"));
Assert.AreEqual("www.google.com", (await w.GetLocation()).Host);
}
}
[TestMethod]
public async Task TestAttrSelection()
{
using (var w = await Driver.Create())
{
await w.NavigateTo(new Uri("http://www.mediafire.com/file/agiqzm1xwebczpx/WABBAJACK_TEST_FILE.tx"));
Assert.IsTrue((await w.GetAttr("a.input", "href")).StartsWith("http://"));
}
}
*/
}
}