У Вас проблема выбора языка. Вы пользуетесь достаточно архаичным (хотя и очень выразительным) языком и переносите его возрастные недостатки на все остальные. Создатели C#, например вашими же словами говорят про «что надо ставить. или ->».
По поводу вставки конструкций — в VS существуют шаблоны, только я ими например не пользуюсь. Мне проще набрать while()\n{\n} нажать Ctrl+(e, d) и получить отформатированный элемент менее чем за 2 секунды (и это без R#). Чем искать наощупь мышь, в нужном месте кликать RMouse->Шаблоны->if(...) за минимум 5 сек. Да и попахивает это делитанством каким-то.
неоднозначности толкования токенов — то ли переменная не объявлена, то ли это продолжение названия переменной, то ли еще что.
Хуже. Не однозначное толкование разделителей. Проблемы начинаются на уровне лексического анализатора. Придется строить грамматики оперирующие отдельными символами.
Ни кто из нас не знает всех функций родного ему Framework-а. Написать аналог существующей — еще не значит написать кусок говнокода. Критерии качества кода — места его в системе, простота замены функции на нечто иное (например, на малоизвестную стандартную), декомпозиция решения (если это не две строки, а например 100) и т.д.
«Это значит, что (при корректном коде) вы просто никогда не попадете во второй шаг сценария.»
Вы правы в этом и смысл проверки — коллизии нет, потому что в одно время этот код обрабатывает только один поток. Если же шаг с timeout-ум выполнится — будет исключение.
1. Логичность названия:
У слова «timeout» — нет перевода «упасть». Первый аргумент в конструкторе называется timeout (и означает именно то как переводится), и он связязан с timeout-ом из BreakMark (на этом шаге мы свалимся по timeout-у). При выполнении сценария мы дожидаемся входа потока (конкретного или любого) в брейкпоинт и выпускаем его. Флаг BreakMark .Timeout, говорит что условий для отпускания не возникнет.
3. Одного шаблона AAA маловато, что бы начать заморачиваться Assert-ами. Шаблон по определению — это вариант, а не догма. Вот если бы мне вдруг понадобилось делать ветвления в сценарии (пока что это выглядит странным), то возможно я бы и занялся переделкой интерфейса.
4. «что не было добавления в очередь». В этом есть логика. Но это не отменит необходимости написать «finish add» (для блокировки выхода) и сделает необходимым добавление одного moc-описания (с обязательными интерфейсом и явной реализацией — породит две сущности, одна из которых будет располагаться в код under test).
«того факта, что второй экземпляр был вызван до того, как мы вышли из первого»
new BreakMark(act0, "precancel prev task"),
new BreakMark(act1, "precancel prev task") { Timeout = true },
new BreakMark(act0, "finish add")
Вторая строчка утверждает недостижимость «precancel prev task» для act1, третья — препятствует выходу из «finish add» (6, 7 свойство автоматических брейкпоинтов, конец статьи).
Большинство финишных Assert проверок из нашего разговора добавлены исключительно для вас. В боевых тестах они встречаются редко. В основном для проверки множественного входа. Обычно я удовлетворяюсь проходимостью сценария, заданного последовательностью брейкпоинтов.
По поводу «модифицировать код class under test» — мне известно. Обратите внимание первые три теста обошлись без этого. Именно для ослабления эффекта Breakpoint.Define помечен [Conditional(«DEBUG»)], а класс Breakpoint вынесен в отдельную сборку (приложенный Solution). По «модифицировать код class under test» можно обсудить. Вопрос тоже не такой простой, хотя что-то новое мы вряд ли скажем. Но вдруг…
DEBUG вы уже ругали. Дискуссия о нем заведет нас в такие дебри, что и трех веток будет мало. Если хотите, можем об этом поговорить отдельно (например в отдельном блоге — думаю количество участников и дельты рейтингов зашкалят).
Изначально мы с вами выделили 5 тестов, а реализовывали только 3. Четвертый это: "«одновременно» добавить две задачи, удостовериться, что в очередь встала одна (любая, потому что поведение компонента не определено) из них". Самый простой способ сделать так:
[TestMethod]
public void SyncCancelPrevTaskTest()
{
using (var disp = new TaskDispatcher())
{
Action act0 = () => disp.Add(TimeSpan.Zero, () => { });
Action act1 = () => disp.Add(TimeSpan.Zero, () => { });
new ThreadTestManager(TimeSpan.FromSeconds(1), act0, act1).Run(
new BreakMark(act0, "precancel prev task"),
new BreakMark(act1, "precancel prev task") { Timeout = true },
new BreakMark(act0, "finish add"));
}
}
Изменения в Add:
class TaskDispatcher : IDisposable
{
object m_syncAdd = new object();
...
public void Add(TimeSpan delay, Action act)
{
lock (m_syncAdd)
{
Breakpoint.Define("precancel prev task");
m_oldCancelEv.Set();
var newCancelEv = new AutoResetEvent(false);
var task = new Task(() => Run(newCancelEv, delay.Ticks > 0 ? delay : TimeSpan.Zero, act));
task.Start();
m_oldCancelEv = newCancelEv;
Breakpoint.Define("finish add");
}
}
}
Что-то у нас разговор сваливается в непонятное русло («в интернете кто-то не прав» с обоих сторон). Предлагаю остаться при своих. Что бы быть честным выкладываю свой только что сделанный вариант решения, для первых трех тестов (и мой вариант этих тестов соответственно). Рабочий больше и сложнее. Поругаем его немного и разойдемся.
[TestMethod]
public void ExecTaskTest()
{
using (var disp = new TaskDispatcher())
{
DateTime add = DateTime.Now;
var execLog = new List<DateTime>();
Action addTask = () =>
disp.Add(TimeSpan.FromSeconds(1),
() =>
{
execLog.Add(DateTime.Now);
Breakpoint.Define("exec");
});
new ThreadTestManager(TimeSpan.FromSeconds(2), addTask).Run("exec");
Assert.IsTrue(execLog.Single() - add > TimeSpan.FromSeconds(1));
}
}
[TestMethod]
public void ExecTwoTaskTest()
{
using (var disp = new TaskDispatcher())
{
var log = new List<int>();
Action act = () =>
{
disp.Add(TimeSpan.FromSeconds(1), () => { log.Add(0); Breakpoint.Define("exec0"); });
Breakpoint.Define("pause");
disp.Add(TimeSpan.FromSeconds(1), () => { log.Add(1); Breakpoint.Define("exec1"); });
};
new ThreadTestManager(TimeSpan.FromSeconds(3), act).Run("exec0", "pause", "exec1");
Assert.AreEqual(2, log.Count);
Assert.AreEqual(0, log[0]);
Assert.AreEqual(1, log[1]);
}
}
[TestMethod]
public void ReplaceTaskTest()
{
using (var disp = new TaskDispatcher())
{
var log = new List<int>();
Action act = () =>
{
disp.Add(TimeSpan.FromSeconds(1), () => { log.Add(0); Breakpoint.Define("exec"); });
disp.Add(TimeSpan.FromSeconds(1), () => { log.Add(1); Breakpoint.Define("exec"); });
};
new ThreadTestManager(TimeSpan.FromSeconds(3), act).Run(
"exec", new BreakMark("exec") { Timeout = true });
Assert.AreEqual(1, log.Count);
Assert.AreEqual(1, log[0]);
}
}
class TaskDispatcher : IDisposable
{
EventWaitHandle m_oldCancelEv;
public TaskDispatcher()
{
m_oldCancelEv = new AutoResetEvent(false);
}
void Run(EventWaitHandle cancelEv, TimeSpan delay, Action act)
{
if (!cancelEv.WaitOne(delay))
act();
}
public void Add(TimeSpan delay, Action act)
{
m_oldCancelEv.Set();
var newCancelEv = new AutoResetEvent(false);
var task = new Task(() => Run(newCancelEv, delay.Ticks > 0 ? delay : TimeSpan.Zero, act));
task.Start();
m_oldCancelEv = newCancelEv;
}
public void Dispose()
{
m_oldCancelEv.Set();
}
}
var t = new Task(() => worker(task, _currentTaskCancellation.Token), _currentTaskCancellation.Token);
t.Start();
<source>
Приоретизация действительно не решена:
<source lang="cs">
[TestMethod]
public void MultipleTasks_ShouldQueueInSequence()
{
var timeService = new TimeServiceStub();
var queue = new QueueMock();
var target = new DelayedExecution(timeService, queue);
var task1 = new TaskStub();
var task2 = new TaskStub();
target.Add(task1);
timeService.Wait(t/2);
target.Add(task2);
timeService.Wait(t);
queue.AssertEnqueuedOnlyOne(task2);
}
«Как это без тестов? А что за пять тестов выше, с которыми вы согласились?»
А вот это про котят :) Не пять, а три (в коде). При представленной вами реализации сомневаюсь что они срабатывают (код ни как не учитывает третьего теста; да и первые два сомнительно — ведь сценарий вы ограничили перекладыванием задач в очередь, чего не сделали).
Ну и удар ниже пояса:
"… в среднем лучше всего свести задачу до того уровня, когда вся мультитредность лежит на фреймворке, а весь код вокруг него — синхронный. И тестировать синхронно."
«Просто обратите внимание, что тест прекрасно обошелся без вашего BreakpointManager»
При этом не протестировав ничего. Была у нас задача накопления task-ов, она и осталась. Была задача по приоритезации, она так же не куда не делась. Все чего мы добились тремя тестами — это перешли с уровня модуля «класс», к уровню «публичный метод» (остальные это часть черного ящика).
А затем «на зубах»(с) написали решение. В правильности которого ни какой уверенности нет, потому что оно без тестов. Страх перед рефакторингом на том же уровне. Дальнейшее эволюционное развитие представляется затруднительным.
Ну тесты без реализации это нонсенс. В TDD они движущая сила построения архитектуры. Давайте все таки попробуем реализовать его, хотя бы так, что бы мы оба одинаково поняли как это сделать.
А то по моему мы пришли к исходному состоянию не решенности задачи.
Остается только target.Add(task2). Давайте тогда его посмотрим. Он должен сохранить где-то на себе элемент и через заданное время перетянуть его на очередь. Даже без замены задач — это не так просто сделать. Мы оказываемся в исходной точке.
По поводу вставки конструкций — в VS существуют шаблоны, только я ими например не пользуюсь. Мне проще набрать while()\n{\n} нажать Ctrl+(e, d) и получить отформатированный элемент менее чем за 2 секунды (и это без R#). Чем искать наощупь мышь, в нужном месте кликать RMouse->Шаблоны->if(...) за минимум 5 сек. Да и попахивает это делитанством каким-то.
Хуже. Не однозначное толкование разделителей. Проблемы начинаются на уровне лексического анализатора. Придется строить грамматики оперирующие отдельными символами.
Вы правы в этом и смысл проверки — коллизии нет, потому что в одно время этот код обрабатывает только один поток. Если же шаг с timeout-ум выполнится — будет исключение.
У слова «timeout» — нет перевода «упасть». Первый аргумент в конструкторе называется timeout (и означает именно то как переводится), и он связязан с timeout-ом из BreakMark (на этом шаге мы свалимся по timeout-у). При выполнении сценария мы дожидаемся входа потока (конкретного или любого) в брейкпоинт и выпускаем его. Флаг BreakMark .Timeout, говорит что условий для отпускания не возникнет.
2. «что мешает коду в потоке 0 успеть пройти всю линию выполнения до момента строчки 2»
«третья — препятствует выходу из «finish add»». Пункт 7 свойств
Каждый следующий шаг сценария начинается, только после полного завершения предыдущего.
3. Одного шаблона AAA маловато, что бы начать заморачиваться Assert-ами. Шаблон по определению — это вариант, а не догма. Вот если бы мне вдруг понадобилось делать ветвления в сценарии (пока что это выглядит странным), то возможно я бы и занялся переделкой интерфейса.
4. «что не было добавления в очередь». В этом есть логика. Но это не отменит необходимости написать «finish add» (для блокировки выхода) и сделает необходимым добавление одного moc-описания (с обязательными интерфейсом и явной реализацией — породит две сущности, одна из которых будет располагаться в код under test).
Вторая строчка утверждает недостижимость «precancel prev task» для act1, третья — препятствует выходу из «finish add» (6, 7 свойство автоматических брейкпоинтов, конец статьи).
Большинство финишных Assert проверок из нашего разговора добавлены исключительно для вас. В боевых тестах они встречаются редко. В основном для проверки множественного входа. Обычно я удовлетворяюсь проходимостью сценария, заданного последовательностью брейкпоинтов.
По поводу «модифицировать код class under test» — мне известно. Обратите внимание первые три теста обошлись без этого. Именно для ослабления эффекта Breakpoint.Define помечен [Conditional(«DEBUG»)], а класс Breakpoint вынесен в отдельную сборку (приложенный Solution). По «модифицировать код class under test» можно обсудить. Вопрос тоже не такой простой, хотя что-то новое мы вряд ли скажем. Но вдруг…
DEBUG вы уже ругали. Дискуссия о нем заведет нас в такие дебри, что и трех веток будет мало. Если хотите, можем об этом поговорить отдельно (например в отдельном блоге — думаю количество участников и дельты рейтингов зашкалят).
Изменения в Add:
«Как это без тестов? А что за пять тестов выше, с которыми вы согласились?»
А вот это про котят :) Не пять, а три (в коде). При представленной вами реализации сомневаюсь что они срабатывают (код ни как не учитывает третьего теста; да и первые два сомнительно — ведь сценарий вы ограничили перекладыванием задач в очередь, чего не сделали).
Ну и удар ниже пояса:
"… в среднем лучше всего свести задачу до того уровня, когда вся мультитредность лежит на фреймворке, а весь код вокруг него — синхронный. И тестировать синхронно."
При этом не протестировав ничего. Была у нас задача накопления task-ов, она и осталась. Была задача по приоритезации, она так же не куда не делась. Все чего мы добились тремя тестами — это перешли с уровня модуля «класс», к уровню «публичный метод» (остальные это часть черного ящика).
А затем «на зубах»(с) написали решение. В правильности которого ни какой уверенности нет, потому что оно без тестов. Страх перед рефакторингом на том же уровне. Дальнейшее эволюционное развитие представляется затруднительным.
А то по моему мы пришли к исходному состоянию не решенности задачи.
Как тогда?
Я даже понял как вы будете реализовывать эту логику.
Если не возражаете, давайте теперь реализуем timeService. Помоему, он наиболее интересен. Так же тестами.