From 141f6c83a184504c0419cd4a2e5dee4b6a550ad0 Mon Sep 17 00:00:00 2001 From: sfaqer Date: Wed, 30 Sep 2026 19:01:17 +1000 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=B0=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=81=D0=BE=D0=BB?= =?UTF-8?q?=D0=B8=20=D0=B2=20=D1=81=D0=B2=D0=BE=D0=B5=D0=BC=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=BA=D0=B5=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit УстановитьПотокВывода и УстановитьПотокОшибок подменяли вывод на весь процесс. Теперь цель вывода у каждого потока выполнения своя, фоновые задания и обработчики запросов веб-сервера наследуют ее от запустившего. Добавлены ПолучитьПотокВывода и ПолучитьПотокОшибок; у потока ошибок появились AutoFlush и кодировка вывода; Сообщить выводит строку одной записью. Смена КодировкаВыходногоПотока действует и после перенаправления. Co-Authored-By: Claude Opus 5.5 --- .../Text/ConsoleContext.cs | 62 +++-- .../Text/ConsoleOutputTargets.cs | 35 +++ .../Text/ConsoleWriterRouter.cs | 230 ++++++++++++++++++ src/OneScript.Web.Server/WebServer.cs | 11 + .../WebServerOutputTests.cs | 125 ++++++++++ .../ConsoleRedirectionTests.cs | 96 ++++++++ src/oscript/Output.cs | 4 +- tests/console.os | 212 ++++++++++++++++ 8 files changed, 756 insertions(+), 19 deletions(-) create mode 100644 src/OneScript.StandardLibrary/Text/ConsoleOutputTargets.cs create mode 100644 src/OneScript.StandardLibrary/Text/ConsoleWriterRouter.cs create mode 100644 src/Tests/OneScript.Core.Tests/WebServerOutputTests.cs create mode 100644 src/Tests/OneScript.StandardLibrary.Tests/ConsoleRedirectionTests.cs diff --git a/src/OneScript.StandardLibrary/Text/ConsoleContext.cs b/src/OneScript.StandardLibrary/Text/ConsoleContext.cs index 2ebe5f7c3..6648f666b 100644 --- a/src/OneScript.StandardLibrary/Text/ConsoleContext.cs +++ b/src/OneScript.StandardLibrary/Text/ConsoleContext.cs @@ -11,6 +11,7 @@ This Source Code Form is subject to the terms of the using OneScript.Exceptions; using OneScript.Execution; using OneScript.StandardLibrary.Binary; +using OneScript.Types; using OneScript.Values; using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; @@ -181,7 +182,7 @@ public IValue OutputEncoding } set { - Console.OutputEncoding = TextEncodingEnum.GetEncoding(value); + ConsoleWriterRouter.SetOutputEncoding(TextEncodingEnum.GetEncoding(value)); } } @@ -229,34 +230,61 @@ public GenericStream OpenStandardOutput() } /// - /// Глобально переопределяет стандартный вывод и направляет в другой поток + /// Переопределяет стандартный вывод и направляет в другой поток. + /// Действует в текущем потоке выполнения и в фоновых заданиях, запущенных из него после вызова. /// - /// Поток назначения + /// Поток назначения. Неопределено - вернуть стандартный вывод [ContextMethod("УстановитьПотокВывода", "SetOutput")] public void SetOutput(IValue target) { - if (!(target.AsObject() is IStreamWrapper stream)) - throw RuntimeException.InvalidArgumentType(nameof(target)); - - var writer = new StreamWriter(stream.GetUnderlyingStream(), Console.OutputEncoding) - { - AutoFlush = true, - }; - Console.SetOut(writer); + ConsoleWriterRouter.SetOut(CreateWriter(target), target); } - + /// - /// Глобально переопределяет стандартный поток ошибок и направляет в другой поток + /// Возвращает поток, в который перенаправлен стандартный вывод текущего потока выполнения, + /// в том числе унаследованный фоновым заданием от запустившего его кода. /// - /// Поток назначения + /// Поток, Неопределено - вывод не перенаправлен + [ContextMethod("ПолучитьПотокВывода", "GetOutput")] + public IValue GetOutput() + { + return ConsoleWriterRouter.GetOutSource() ?? BslUndefinedValue.Instance; + } + + /// + /// Переопределяет стандартный поток ошибок и направляет в другой поток. + /// Действует в текущем потоке выполнения и в фоновых заданиях, запущенных из него после вызова. + /// + /// Поток назначения. Неопределено - вернуть стандартный поток ошибок [ContextMethod("УстановитьПотокОшибок", "SetError")] public void SetError(IValue target) { + ConsoleWriterRouter.SetError(CreateWriter(target), target); + } + + /// + /// Возвращает поток, в который перенаправлен поток ошибок текущего потока выполнения, + /// в том числе унаследованный фоновым заданием от запустившего его кода. + /// + /// Поток, Неопределено - поток ошибок не перенаправлен + [ContextMethod("ПолучитьПотокОшибок", "GetError")] + public IValue GetError() + { + return ConsoleWriterRouter.GetErrorSource() ?? BslUndefinedValue.Instance; + } + + private static StreamWriter CreateWriter(IValue target) + { + if (target.SystemType == BasicTypes.Undefined) + return null; + if (!(target.AsObject() is IStreamWrapper stream)) throw RuntimeException.InvalidArgumentType(nameof(target)); - - var writer = new StreamWriter(stream.GetUnderlyingStream()); - Console.SetError(writer); + + return new StreamWriter(stream.GetUnderlyingStream(), Console.OutputEncoding) + { + AutoFlush = true, + }; } public const string ConsoleCancelKeyEvent = "CancelKeyPressed"; diff --git a/src/OneScript.StandardLibrary/Text/ConsoleOutputTargets.cs b/src/OneScript.StandardLibrary/Text/ConsoleOutputTargets.cs new file mode 100644 index 000000000..6bda09b77 --- /dev/null +++ b/src/OneScript.StandardLibrary/Text/ConsoleOutputTargets.cs @@ -0,0 +1,35 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +namespace OneScript.StandardLibrary.Text +{ + /// + /// Куда текущий поток выполнения выводит консоль (Консоль.УстановитьПотокВывода/УстановитьПотокОшибок). + /// Фоновые задания наследуют это сами, а там, куда контекст выполнения не передается + /// (обработчики запросов веб-сервера), цели нужно перенести явно. + /// + public sealed class ConsoleOutputTargets + { + private readonly (object Out, object Error) _targets; + + private ConsoleOutputTargets((object Out, object Error) targets) + { + _targets = targets; + } + + /// + /// Запоминает цели вывода текущего потока выполнения + /// + public static ConsoleOutputTargets Capture() => new ConsoleOutputTargets(ConsoleWriterRouter.CaptureTargets()); + + /// + /// Устанавливает запомненные цели в текущем потоке выполнения. + /// Вызванный внутри async-метода, действует только до его завершения. + /// + public void Apply() => ConsoleWriterRouter.ApplyTargets(_targets); + } +} diff --git a/src/OneScript.StandardLibrary/Text/ConsoleWriterRouter.cs b/src/OneScript.StandardLibrary/Text/ConsoleWriterRouter.cs new file mode 100644 index 000000000..5aa662407 --- /dev/null +++ b/src/OneScript.StandardLibrary/Text/ConsoleWriterRouter.cs @@ -0,0 +1,230 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.IO; +using System.Text; +using System.Threading; +using ScriptEngine.Machine; + +namespace OneScript.StandardLibrary.Text +{ + /// + /// Стандартный вывод или поток ошибок консоли со своей целью у каждого потока выполнения. + /// Цель хранится в AsyncLocal: фоновые задания наследуют ее от запустившего их кода, + /// а переопределение внутри задания не затрагивает остальных. + /// + internal sealed class ConsoleWriterRouter : TextWriter + { + private static readonly object InstallLock = new object(); + private static ConsoleWriterRouter _out; + private static ConsoleWriterRouter _error; + + private readonly AsyncLocal _current = new AsyncLocal(); + // Открывает стандартный поток, если вывод по умолчанию - собственный вывод консоли, а не заданный хостом + private readonly Func _openConsoleStream; + private volatile TextWriter _default; + private TextWriter _installed; + + private ConsoleWriterRouter(TextWriter defaultWriter, Func openConsoleStream) + { + _default = defaultWriter; + _openConsoleStream = openConsoleStream; + } + + /// + /// Направляет стандартный вывод текущего потока выполнения в writer; null возвращает вывод по умолчанию. + /// + public static void SetOut(TextWriter writer, IValue source) + { + lock (InstallLock) + { + // Console.Out мог заменить хост: тогда он становится выводом по умолчанию + if (!IsInstalled(_out, Console.Out)) + { + if (writer == null) + return; + + var isConsoleWriter = IsConsoleWriter(() => Console.Out); + _out = new ConsoleWriterRouter(Console.Out, isConsoleWriter ? Console.OpenStandardOutput : null); + Console.SetOut(_out); + _out._installed = Console.Out; + } + + _out.SetCurrent(writer, source); + } + } + + /// + /// Направляет поток ошибок текущего потока выполнения в writer; null возвращает поток ошибок по умолчанию. + /// + public static void SetError(TextWriter writer, IValue source) + { + lock (InstallLock) + { + if (!IsInstalled(_error, Console.Error)) + { + if (writer == null) + return; + + var isConsoleWriter = IsConsoleWriter(() => Console.Error); + _error = new ConsoleWriterRouter(Console.Error, isConsoleWriter ? Console.OpenStandardError : null); + Console.SetError(_error); + _error._installed = Console.Error; + } + + _error.SetCurrent(writer, source); + } + } + + /// + /// Меняет кодировку вывода консоли, в том числе вывода по умолчанию у установленных роутеров. + /// + public static void SetOutputEncoding(Encoding encoding) + { + lock (InstallLock) + { + Console.OutputEncoding = encoding; + + // Console пересоздает свой вывод с новой кодировкой, только пока его не подменили через SetOut + _out?.RecreateConsoleWriter(); + _error?.RecreateConsoleWriter(); + } + } + + // Цели текущего потока выполнения - для мест, куда контекст выполнения не передается сам + internal static (object Out, object Error) CaptureTargets() + { + lock (InstallLock) + { + return (_out?._current.Value, _error?._current.Value); + } + } + + internal static void ApplyTargets((object Out, object Error) targets) + { + lock (InstallLock) + { + if (_out != null) + _out._current.Value = (Target)targets.Out; + if (_error != null) + _error._current.Value = (Target)targets.Error; + } + } + + /// + /// Поток, в который перенаправлен стандартный вывод текущего потока выполнения, или null. + /// + public static IValue GetOutSource() => GetSource(_out, Console.Out); + + /// + /// Поток, в который перенаправлен поток ошибок текущего потока выполнения, или null. + /// + public static IValue GetErrorSource() => GetSource(_error, Console.Error); + + private static bool IsInstalled(ConsoleWriterRouter router, TextWriter console) + => router != null && ReferenceEquals(console, router._installed); + + private static IValue GetSource(ConsoleWriterRouter router, TextWriter console) + => IsInstalled(router, console) ? router._current.Value?.Source : null; + + // Публично не узнать, подменял ли хост вывод консоли. Но при смене кодировки Console пересоздает + // только свой вывод: присваиваем ту же кодировку и смотрим, сменился ли объект + private static bool IsConsoleWriter(Func consoleWriter) + { + var writer = consoleWriter(); + try + { + var encoding = Console.OutputEncoding; + Console.OutputEncoding = encoding; + } + catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is PlatformNotSupportedException) + { + // Без консоли кодировку не сменить, и пересоздавать вывод не придется + return false; + } + + return !ReferenceEquals(writer, consoleWriter()); + } + + private void RecreateConsoleWriter() + { + if (_openConsoleStream == null) + return; + + var previous = _default; + _default = CreateConsoleWriter(_openConsoleStream()); + previous.Flush(); + } + + // Как свой вывод создает Console: без BOM и со сбросом после каждой записи + private static TextWriter CreateConsoleWriter(Stream stream) + { + var writer = new StreamWriter(stream, WithoutPreamble(Console.OutputEncoding), 256, leaveOpen: true) + { + AutoFlush = true + }; + return TextWriter.Synchronized(writer); + } + + private static Encoding WithoutPreamble(Encoding encoding) + { + if (encoding.GetPreamble().Length == 0) + return encoding; + + return encoding.CodePage switch + { + 65001 => new UTF8Encoding(false), + 1200 => new UnicodeEncoding(false, false), + 1201 => new UnicodeEncoding(true, false), + 12000 => new UTF32Encoding(false, false), + 12001 => new UTF32Encoding(true, false), + _ => encoding + }; + } + + private void SetCurrent(TextWriter writer, IValue source) + { + _current.Value = writer == null ? null : new Target(writer, source); + } + + private TextWriter Writer => _current.Value?.Writer ?? _default; + + public override Encoding Encoding => Writer.Encoding; + + public override IFormatProvider FormatProvider => Writer.FormatProvider; + + public override void Write(char value) => Writer.Write(value); + + public override void Write(char[] buffer, int index, int count) => Writer.Write(buffer, index, count); + + public override void Write(ReadOnlySpan buffer) => Writer.Write(buffer); + + public override void Write(string value) => Writer.Write(value); + + public override void WriteLine() => Writer.WriteLine(); + + public override void WriteLine(ReadOnlySpan buffer) => Writer.WriteLine(buffer); + + public override void WriteLine(string value) => Writer.WriteLine(value); + + public override void Flush() => Writer.Flush(); + + private sealed class Target + { + public Target(TextWriter writer, IValue source) + { + Writer = writer; + Source = source; + } + + public TextWriter Writer { get; } + + public IValue Source { get; } + } + } +} diff --git a/src/OneScript.Web.Server/WebServer.cs b/src/OneScript.Web.Server/WebServer.cs index d06a876b9..68e5a2a50 100644 --- a/src/OneScript.Web.Server/WebServer.cs +++ b/src/OneScript.Web.Server/WebServer.cs @@ -9,6 +9,7 @@ This Source Code Form is subject to the terms of the using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using OneScript.Contexts; +using OneScript.StandardLibrary.Text; using OneScript.Types; using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; @@ -123,6 +124,16 @@ private void ConfigureApp() _app = builder.Build(); + // Обработчики запросов, как и фоновые задания, выводят консоль туда же, куда тот, + // кто запустил сервер. Переносятся только цели вывода, остальной контекст запроса + // (трассировка и т.п.) остается свой; async - чтобы цели не вышли за пределы запроса + var consoleTargets = ConsoleOutputTargets.Capture(); + _app.Use(async (context, next) => + { + consoleTargets.Apply(); + await next(); + }); + if (_useStaticFiles) _app.UseStaticFiles(); diff --git a/src/Tests/OneScript.Core.Tests/WebServerOutputTests.cs b/src/Tests/OneScript.Core.Tests/WebServerOutputTests.cs new file mode 100644 index 000000000..c0c1de89c --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/WebServerOutputTests.cs @@ -0,0 +1,125 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System.IO; +using System.Net; +using System.Net.Sockets; +using FluentAssertions; +using OneScript.Contexts; +using OneScript.StandardLibrary; +using OneScript.StandardLibrary.Binary; +using OneScript.Web.Server; +using ScriptEngine.HostedScript; +using ScriptEngine.HostedScript.Extensions; +using ScriptEngine.Hosting; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; +using Xunit; + +namespace OneScript.Core.Tests +{ + [GlobalContext(ManualRegistration = true)] + public class RequestTraceProbe : GlobalContextBase + { + [ContextMethod("ТрассировкаЗапроса")] + public string CurrentActivity() => System.Diagnostics.Activity.Current?.Id ?? ""; + } + + public class WebServerOutputTests + { + private const string Script = + "Перем мСервер;\n" + + "\n" + + "Функция ОбработчикЗапроса(Контекст, СледующийОбработчик) Экспорт\n" + + " Консоль.ВывестиСтроку(\"from request\");\n" + + " Контекст.Ответ.КодСостояния = 200;\n" + + "КонецФункции\n" + + "\n" + + "Процедура ЗапуститьСервер() Экспорт\n" + + " мСервер.Запустить();\n" + + "КонецПроцедуры\n" + + "\n" + + "мСервер = Новый ВебСервер(Порт);\n" + + "мСервер.ДобавитьОбработчикЗапросов(ЭтотОбъект, \"ОбработчикЗапроса\");\n" + + "Консоль.УстановитьПотокВывода(Вывод);\n" + + "Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, \"ЗапуститьСервер\");\n" + + "Соединение = Новый HTTPСоединение(\"http://127.0.0.1:\" + Формат(Порт, \"ЧГ=\"));\n" + + "Для Номер = 1 По 100 Цикл\n" + + " Попытка\n" + + " Соединение.Получить(Новый HTTPЗапрос(\"/\"));\n" + + " Прервать;\n" + + " Исключение\n" + + " Приостановить(100);\n" + + " КонецПопытки;\n" + + "КонецЦикла;\n" + + "мСервер.Остановить();\n" + + "Задание.ОжидатьЗавершения(10000);\n"; + + [Fact] + public void RequestHandlersWriteToOutputOfServerStarter() + { + var engine = CreateEngine(); + var output = MemoryStreamContext.Constructor(); + var context = new ExternalContextData + { + { "Порт", ValueFactory.Create(FreePort()) }, + { "Вывод", output } + }; + + engine.Engine.AttachedScriptsFactory.LoadFromString( + engine.GetCompilerService(), Script, engine.Engine.NewProcess(), context); + + // Текст латиницей: кодировка консоли на агентах CI может не содержать кириллицу + var bytes = ((MemoryStream)output.GetUnderlyingStream()).ToArray(); + using var reader = new StreamReader(new MemoryStream(bytes), System.Console.OutputEncoding, true); + reader.ReadToEnd().Should().Contain("from request"); + } + + [Fact] + public void RequestHandlersKeepRequestExecutionContext() + { + var engine = CreateEngine(); + engine.Engine.Environment.InjectObject(new RequestTraceProbe()); + var context = new ExternalContextData + { + { "Порт", ValueFactory.Create(FreePort()) }, + { "Вывод", MemoryStreamContext.Constructor() } + }; + + var script = Script.Replace( + " Консоль.ВывестиСтроку(\"from request\");\n", + " Трассировка = ТрассировкаЗапроса();\n"); + var instance = engine.Engine.AttachedScriptsFactory.LoadFromString( + engine.GetCompilerService(), "Перем Трассировка Экспорт;\n" + script, engine.Engine.NewProcess(), context); + + // Активность запроса ASP.NET Core хранится в AsyncLocal контекста запроса: + // перенос вывода консоли не должен подменять этот контекст контекстом старта сервера + instance.GetPropValue(instance.GetPropertyNumber("Трассировка")).ToString().Should().NotBeEmpty(); + } + + private static HostedScriptEngine CreateEngine() + { + var builder = DefaultEngineBuilder.Create() + .SetDefaultOptions() + .UseImports() + .UseDefaultHosting() + .SetupEnvironment(e => e.AddStandardLibrary().AddWebServer()); + var engine = new HostedScriptEngine(builder.Build()); + engine.Initialize(); + return engine; + } + + private static int FreePort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + } +} diff --git a/src/Tests/OneScript.StandardLibrary.Tests/ConsoleRedirectionTests.cs b/src/Tests/OneScript.StandardLibrary.Tests/ConsoleRedirectionTests.cs new file mode 100644 index 000000000..6fd827fd1 --- /dev/null +++ b/src/Tests/OneScript.StandardLibrary.Tests/ConsoleRedirectionTests.cs @@ -0,0 +1,96 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using FluentAssertions; +using OneScript.StandardLibrary.Binary; +using OneScript.StandardLibrary.Text; +using Xunit; + +namespace OneScript.StandardLibrary.Tests +{ + public class ConsoleRedirectionTests + { + [Fact] + public void ErrorStreamIsWrittenWithoutFlush() + { + var console = new ConsoleContext(null); + var target = MemoryStreamContext.Constructor(); + + RunInThreads(1, _ => + { + console.SetError(target); + Console.Error.Write("ошибка"); + }); + + ReadText(target).Should().Be("ошибка"); + } + + [Fact] + public void EachThreadWritesToItsOwnErrorStream() + { + const int threadsCount = 8; + var console = new ConsoleContext(null); + var targets = Enumerable.Range(0, threadsCount).Select(_ => MemoryStreamContext.Constructor()).ToArray(); + using var barrier = new Barrier(threadsCount); + + RunInThreads(threadsCount, number => + { + console.SetError(targets[number]); + // Все потоки перенаправлены до того, как кто-то начнет писать + barrier.SignalAndWait(); + for (var i = 0; i < 100; i++) + { + Console.Error.WriteLine($"поток {number}"); + } + }); + + for (var number = 0; number < threadsCount; number++) + { + ReadText(targets[number]).Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) + .Should().HaveCount(100).And.OnlyContain(x => x == $"поток {number}"); + } + } + + private static void RunInThreads(int threadsCount, Action action) + { + Exception error = null; + var threads = Enumerable.Range(0, threadsCount).Select(number => new Thread(() => + { + try + { + action(number); + } + catch (Exception e) + { + error = e; + } + })).ToArray(); + + foreach (var thread in threads) + { + thread.Start(); + } + foreach (var thread in threads) + { + thread.Join(); + } + + error.Should().BeNull(); + } + + private static string ReadText(MemoryStreamContext stream) + { + var bytes = ((MemoryStream)stream.GetUnderlyingStream()).ToArray(); + using var reader = new StreamReader(new MemoryStream(bytes), Console.OutputEncoding, true); + return reader.ReadToEnd(); + } + } +} diff --git a/src/oscript/Output.cs b/src/oscript/Output.cs index 73f026b74..cfcbac82e 100644 --- a/src/oscript/Output.cs +++ b/src/oscript/Output.cs @@ -47,8 +47,8 @@ private static void Init() public static void WriteLine(string text) { - Write(text); - WriteLine(); + // Одной записью: иначе строки из фоновых заданий склеиваются + Write(text + Environment.NewLine); } public static void WriteLine() diff --git a/tests/console.os b/tests/console.os index 2f6429031..45a0ab981 100644 --- a/tests/console.os +++ b/tests/console.os @@ -12,6 +12,13 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьПередачуДанныхВСкриптЧерезСтандартныйПотокВвода"); #КонецЕсли ВсеТесты.Добавить("ТестДолжен_ПроверитьПеренаправлениеВывода"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПеренаправлениеВыводаВФоновыхЗаданиях"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоФоновоеЗаданиеНаследуетПотокВывода"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоСтрокиЗаданийНеСклеиваются"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПолучениеПотокаВывода"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПолучениеПотокаОшибок"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоЗаданиеПолучаетУнаследованныйПотокВывода"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьСменуКодировкиВыводаПослеПеренаправленияВЗадании"); ВсеТесты.Добавить("ТестДолжен_ПроверитЦветКонсоли"); Возврат ВсеТесты; @@ -145,6 +152,211 @@ КонецПроцедуры +Процедура ТестДолжен_ПроверитьПеренаправлениеВыводаВФоновыхЗаданиях() Экспорт + + ВыводТеста = Новый ПотокВПамяти; + ВыводЗаданий = Новый Массив; + Задания = Новый Массив; + + Консоль.УстановитьПотокВывода(ВыводТеста); + Попытка + Для Номер = 1 По 8 Цикл + ВыводЗадания = Новый ПотокВПамяти; + ВыводЗаданий.Добавить(ВыводЗадания); + Параметры = Новый Массив; + Параметры.Добавить(ВыводЗадания); + Параметры.Добавить("задание " + Номер); + Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ВывестиСтрокиВСвойПоток", Параметры)); + КонецЦикла; + // Пока задания пишут в свои потоки + ВывестиСтроки("тест"); + ФоновыеЗадания.ОжидатьВсе(Задания); + Исключение + Консоль.УстановитьПотокВывода(Консоль.ОткрытьСтандартныйПотокВывода()); + ВызватьИсключение; + КонецПопытки; + Консоль.УстановитьПотокВывода(Консоль.ОткрытьСтандартныйПотокВывода()); + + ПроверитьЧтоЗаданияБезОшибок(Задания); + ПроверитьВыведенныеСтроки(ВыводТеста, "тест", КоличествоСтрокВывода()); + Для Номер = 1 По 8 Цикл + ПроверитьВыведенныеСтроки(ВыводЗаданий[Номер - 1], "задание " + Номер, КоличествоСтрокВывода()); + КонецЦикла; + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЧтоФоновоеЗаданиеНаследуетПотокВывода() Экспорт + + Вывод = Новый ПотокВПамяти; + + Консоль.УстановитьПотокВывода(Вывод); + Попытка + Параметры = Новый Массив; + Параметры.Добавить("из задания"); + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ВывестиСтроки", Параметры); + Задание.ОжидатьЗавершения(); + Исключение + Консоль.УстановитьПотокВывода(Консоль.ОткрытьСтандартныйПотокВывода()); + ВызватьИсключение; + КонецПопытки; + Консоль.УстановитьПотокВывода(Консоль.ОткрытьСтандартныйПотокВывода()); + + ПроверитьЧтоЗаданияБезОшибок(МассивИз(Задание)); + ПроверитьВыведенныеСтроки(Вывод, "из задания", КоличествоСтрокВывода()); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЧтоСтрокиЗаданийНеСклеиваются() Экспорт + + Вывод = Новый ПотокВПамяти; + Задания = Новый Массив; + + Консоль.УстановитьПотокВывода(Вывод); + Попытка + Для Номер = 1 По 8 Цикл + Параметры = Новый Массив; + Параметры.Добавить("задание"); + Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ВывестиСтроки", Параметры)); + КонецЦикла; + ФоновыеЗадания.ОжидатьВсе(Задания); + Исключение + Консоль.УстановитьПотокВывода(Консоль.ОткрытьСтандартныйПотокВывода()); + ВызватьИсключение; + КонецПопытки; + Консоль.УстановитьПотокВывода(Консоль.ОткрытьСтандартныйПотокВывода()); + + ПроверитьЧтоЗаданияБезОшибок(Задания); + ПроверитьВыведенныеСтроки(Вывод, "задание", 8 * КоличествоСтрокВывода()); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПолучениеПотокаВывода() Экспорт + + Прежний = Консоль.ПолучитьПотокВывода(); + Лог = Новый ПотокВПамяти; + + Консоль.УстановитьПотокВывода(Лог); + Текущий = Консоль.ПолучитьПотокВывода(); + Консоль.УстановитьПотокВывода(Неопределено); + ПослеСброса = Консоль.ПолучитьПотокВывода(); + Консоль.УстановитьПотокВывода(Прежний); + + юТест.ПроверитьРавенство(Лог, Текущий); + юТест.ПроверитьРавенство(Неопределено, ПослеСброса); + юТест.ПроверитьРавенство(Прежний, Консоль.ПолучитьПотокВывода()); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПолучениеПотокаОшибок() Экспорт + + Прежний = Консоль.ПолучитьПотокОшибок(); + Лог = Новый ПотокВПамяти; + + Консоль.УстановитьПотокОшибок(Лог); + Текущий = Консоль.ПолучитьПотокОшибок(); + Консоль.УстановитьПотокОшибок(Неопределено); + ПослеСброса = Консоль.ПолучитьПотокОшибок(); + Консоль.УстановитьПотокОшибок(Прежний); + + юТест.ПроверитьРавенство(Лог, Текущий); + юТест.ПроверитьРавенство(Неопределено, ПослеСброса); + юТест.ПроверитьРавенство(Прежний, Консоль.ПолучитьПотокОшибок()); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЧтоЗаданиеПолучаетУнаследованныйПотокВывода() Экспорт + + Прежний = Консоль.ПолучитьПотокВывода(); + Лог = Новый ПотокВПамяти; + + Консоль.УстановитьПотокВывода(Лог); + Попытка + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПотокВыводаЗадания"); + Задание.ОжидатьЗавершения(); + Исключение + Консоль.УстановитьПотокВывода(Прежний); + ВызватьИсключение; + КонецПопытки; + Консоль.УстановитьПотокВывода(Прежний); + + ПроверитьЧтоЗаданияБезОшибок(МассивИз(Задание)); + юТест.ПроверитьРавенство(Лог, Задание.Результат); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьСменуКодировкиВыводаПослеПеренаправленияВЗадании() Экспорт + + // В отдельном процессе: кодировка консоли общая на процесс + КодСкрипта = "Процедура Перенаправить() Экспорт + | Консоль.УстановитьПотокВывода(Новый ПотокВПамяти); + |КонецПроцедуры + |ФоновыеЗадания.Выполнить(ЭтотОбъект, ""Перенаправить"").ОжидатьЗавершения(); + |Консоль.КодировкаВыходногоПотока = КодировкаТекста.UTF16; + |Консоль.Вывести(""Привет"");"; + + ТекстСкрипта = Новый ТекстовыйДокумент(); + ТекстСкрипта.УстановитьТекст(КодСкрипта); + ВремФайл = ПолучитьИмяВременногоФайла("os"); + ТекстСкрипта.Записать(ВремФайл); + + ПутьКОскрипт = ОбъединитьПути(КаталогПрограммы(), "oscript.dll"); + Процесс = СоздатьПроцесс(СтрШаблон("dotnet ""%1"" ""%2""", ПутьКОскрипт, ВремФайл), , Истина, , КодировкаТекста.UTF16); + Процесс.Запустить(); + Процесс.ОжидатьЗавершения(); + Вывод = Процесс.ПотокВывода.Прочитать(); + УдалитьФайлы(ВремФайл); + + юТест.ПроверитьРавенство("Привет", Вывод, "Вывод основного потока в новой кодировке"); + +КонецПроцедуры + +Функция ПотокВыводаЗадания() Экспорт + Возврат Консоль.ПолучитьПотокВывода(); +КонецФункции + +Процедура ВывестиСтрокиВСвойПоток(Поток, Текст) Экспорт + Консоль.УстановитьПотокВывода(Поток); + ВывестиСтроки(Текст); +КонецПроцедуры + +Процедура ВывестиСтроки(Текст) Экспорт + Для Счетчик = 1 По КоличествоСтрокВывода() Цикл + Сообщить(Текст); + КонецЦикла; +КонецПроцедуры + +Функция КоличествоСтрокВывода() + Возврат 200; +КонецФункции + +Функция МассивИз(Элемент) + Результат = Новый Массив; + Результат.Добавить(Элемент); + Возврат Результат; +КонецФункции + +Процедура ПроверитьЧтоЗаданияБезОшибок(Задания) + Для Каждого Задание Из Задания Цикл + Если Задание.ИнформацияОбОшибке <> Неопределено Тогда + ВызватьИсключение Задание.ИнформацияОбОшибке.ПодробноеОписаниеОшибки(); + КонецЕсли; + КонецЦикла; +КонецПроцедуры + +Процедура ПроверитьВыведенныеСтроки(Поток, ОжидаемаяСтрока, ОжидаемоеКоличество) + + Поток.Перейти(0, ПозицияВПотоке.Начало); + Чтение = Новый ЧтениеТекста(Поток, Консоль.КодировкаВыходногоПотока); + Строки = СтрРазделить(Чтение.Прочитать(), Символы.ПС, Ложь); + Чтение.Закрыть(); + + юТест.ПроверитьРавенство(ОжидаемоеКоличество, Строки.Количество(), "Количество строк " + ОжидаемаяСтрока); + Для Каждого Строка Из Строки Цикл + юТест.ПроверитьРавенство(ОжидаемаяСтрока, СокрЛП(Строка)); + КонецЦикла; + +КонецПроцедуры + Процедура ТестДолжен_ПроверитЦветКонсоли() Экспорт // Получение цвета не должно упасть