├── .travis.yml ├── LICENSE ├── README.md ├── csharp ├── .gitignore ├── TaskList.sln ├── Tasks.Tests │ ├── ApplicationTest.cs │ ├── BlockingStream.cs │ ├── FakeConsole.cs │ ├── ProducerConsumerStream.cs │ └── Tasks.Tests.csproj └── Tasks │ ├── IConsole.cs │ ├── RealConsole.cs │ ├── Task.cs │ ├── TaskList.cs │ └── Tasks.csproj ├── golang ├── .gitignore ├── README.md ├── build_and_run.sh ├── run_tests.sh └── src │ └── task │ ├── list.go │ ├── main.go │ ├── main_test.go │ └── task.go ├── java ├── .gitignore ├── pom.xml ├── run └── src │ ├── main │ └── java │ │ └── com │ │ └── codurance │ │ └── training │ │ └── tasks │ │ ├── Task.java │ │ └── TaskList.java │ └── test │ └── java │ └── com │ └── codurance │ └── training │ └── tasks │ └── ApplicationTest.java ├── kotlin ├── .gitignore ├── pom.xml ├── run └── src │ ├── main │ └── java │ │ └── com │ │ └── codurance │ │ └── training │ │ └── tasks │ │ ├── Task.kt │ │ └── TaskList.kt │ └── test │ └── java │ └── com │ └── codurance │ └── training │ └── tasks │ └── ApplicationTest.java ├── python ├── .gitignore ├── README.md ├── setup.py ├── task_list │ ├── __init__.py │ ├── __main__.py │ ├── app.py │ ├── console.py │ └── task.py └── tests │ ├── __init__.py │ └── test_application.py ├── ruby ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── lib │ ├── task.rb │ └── task_list.rb └── spec │ └── application_spec.rb ├── scala ├── .gitignore ├── build.sbt └── src │ ├── main │ └── scala │ │ └── TaskList.scala │ └── test │ └── scala │ └── TaskListSpec.scala └── typescript ├── .gitignore ├── README ├── package.json ├── src ├── task.ts └── task_list.ts ├── tests └── application_test.ts └── tsd.json /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | install: 5 | - (cd java && mvn install -DskipTests=true) 6 | script: 7 | - (cd java && mvn test) 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Codurance, Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task List   [![Build Status](https://travis-ci.org/codurance/task-list.png)](https://travis-ci.org/codurance/task-list) 2 | 3 | This is an example of code obsessed with primitives. 4 | 5 | A *primitive* is any concept technical in nature, and not relevant to your business domain. This includes integers, characters, strings, and collections (lists, sets, maps, etc.), but also things like threads, readers, writers, parsers, exceptions, and anything else purely focused on technical concerns. By contrast, the business concepts in this project, "task", "project", etc. should be considered part of your *domain model*. The domain model is the language of the business in which you operate, and using it in your code base helps you avoid speaking different languages, helping you to avoid misunderstandings. In our experience, misunderstandings are the biggest cause of bugs. 6 | 7 | ## Exercise 8 | 9 | Try implementing the following features, refactoring primitives away as you go. Try not to implement any new behaviour until the code you're about to change has been completely refactored to remove primitives, i.e. **_Only refactor the code you're about to change, then make your change. Don't refactor unrelated code._** 10 | 11 | One set of criteria to identify when primitives have been removed is to only allow primitives in constructor parameter lists, and as local variables and private fields. They shouldn't be passed into methods or returned from methods. The only exception is true infrastructure code—code that communicates with the terminal, the network, the database, etc. Infrastructure requires serialisation to primitives, but should be treated as a special case. You could even consider your infrastructure as a separate domain, technical in nature, in which primitives *are* the domain. 12 | 13 | You should try to wrap tests around the behaviour you're refactoring. At the beginning, these will mostly be high-level system tests, but you should find yourself writing more unit tests as you proceed. 14 | 15 | ### Features 16 | 17 | 1. Deadlines 18 | 1. Give each task an optional deadline with the `deadline ` command. 19 | 2. Show all tasks due today with the `today` command. 20 | 2. Customisable IDs 21 | 1. Allow the user to specify an identifier that's not a number. 22 | 2. Disallow spaces and special characters from the ID. 23 | 3. Deletion 24 | 1. Allow users to delete tasks with the `delete ` command. 25 | 4. Views 26 | 1. View tasks by date with the `view by date` command. 27 | 2. View tasks by deadline with the `view by deadline` command. 28 | 3. Don't remove the functionality that allows users to view tasks by project, but change the command to `view by project`. 29 | 30 | ### Considerations and Approaches 31 | 32 | Think about *behaviour attraction*. Quite often, you can reduce the amount of behaviour that relies upon primitives from the outside world (as opposed to internal primitives stored as private fields or locals) simply by moving the behaviour to a *value object* which holds the primitives. If you don't have a value object, create one. These value objects are known as *behaviour attractors* because once they're created, they make it far more obvious where behaviour should live. 33 | 34 | A related principle is to consider the type of object you've created. Is it a true value object (or *record*), which simply consists of `getFoo` methods that return their internal primitives (to be used only with infrastructure, of course), or is it an object with behaviour? If it's the latter, you should avoid exposing any internal state at all. The former should not contain any behaviour. Treating something as both a record and an object generally leads to disaster. 35 | 36 | Your approach will depend on whether you learn toward a functional or an object-oriented style for modelling your domain. Both encourage encapsulation, but *information hiding* techniques are generally only used in object-oriented code. They also differ in the approach used to extract behaviour; functional programming often works with closed sets of behaviour through *tagged unions*, whereas in object-oriented code, we use *polymorphism* to achieve the same ends in an open, extensible manner. 37 | 38 | Separate your commands and queries. Tell an object to do something, or ask it about something, but don't do both. 39 | 40 | Lastly, consider SOLID principles when refactoring: 41 | 42 | * Aim to break large chunks of behaviour into small ones, each with a single responsibility. 43 | * Think about the dimensions in which it should be easy to extend the application. 44 | * Don't surprise your callers. Conform to the interface. 45 | * Segregate behaviour based upon the needs. 46 | * Depend upon abstractions. 47 | -------------------------------------------------------------------------------- /csharp/.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | bin 3 | obj 4 | packages 5 | *.suo 6 | -------------------------------------------------------------------------------- /csharp/TaskList.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30128.74 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tasks", "Tasks\Tasks.csproj", "{15B0CBFF-BE62-420C-878A-94FC6590A01D}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tasks.Tests", "Tasks.Tests\Tasks.Tests.csproj", "{7300E494-0DC7-4233-8732-93D685B0F485}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|Mixed Platforms = Debug|Mixed Platforms 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|Mixed Platforms = Release|Mixed Platforms 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 23 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 24 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Debug|x86.ActiveCfg = Debug|Any CPU 25 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Debug|x86.Build.0 = Debug|Any CPU 26 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 29 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Release|Mixed Platforms.Build.0 = Release|Any CPU 30 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Release|x86.ActiveCfg = Release|Any CPU 31 | {15B0CBFF-BE62-420C-878A-94FC6590A01D}.Release|x86.Build.0 = Release|Any CPU 32 | {7300E494-0DC7-4233-8732-93D685B0F485}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {7300E494-0DC7-4233-8732-93D685B0F485}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {7300E494-0DC7-4233-8732-93D685B0F485}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 35 | {7300E494-0DC7-4233-8732-93D685B0F485}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 36 | {7300E494-0DC7-4233-8732-93D685B0F485}.Debug|x86.ActiveCfg = Debug|Any CPU 37 | {7300E494-0DC7-4233-8732-93D685B0F485}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {7300E494-0DC7-4233-8732-93D685B0F485}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {7300E494-0DC7-4233-8732-93D685B0F485}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 40 | {7300E494-0DC7-4233-8732-93D685B0F485}.Release|Mixed Platforms.Build.0 = Release|Any CPU 41 | {7300E494-0DC7-4233-8732-93D685B0F485}.Release|x86.ActiveCfg = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {E7DD7DB2-3EAD-49DB-9626-0074036608B6} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /csharp/Tasks.Tests/ApplicationTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using NUnit.Framework; 4 | 5 | namespace Tasks 6 | { 7 | [TestFixture] 8 | public sealed class ApplicationTest 9 | { 10 | public const string PROMPT = "> "; 11 | 12 | private FakeConsole console; 13 | private System.Threading.Thread applicationThread; 14 | 15 | [SetUp] 16 | public void StartTheApplication() 17 | { 18 | this.console = new FakeConsole(); 19 | var taskList = new TaskList(console); 20 | this.applicationThread = new System.Threading.Thread(() => taskList.Run()); 21 | applicationThread.Start(); 22 | } 23 | 24 | [TearDown] 25 | public void KillTheApplication() 26 | { 27 | if (applicationThread == null || !applicationThread.IsAlive) 28 | { 29 | return; 30 | } 31 | 32 | applicationThread.Abort(); 33 | throw new Exception("The application is still running."); 34 | } 35 | 36 | [Test, Timeout(1000)] 37 | public void ItWorks() 38 | { 39 | Execute("show"); 40 | 41 | Execute("add project secrets"); 42 | Execute("add task secrets Eat more donuts."); 43 | Execute("add task secrets Destroy all humans."); 44 | 45 | Execute("show"); 46 | ReadLines( 47 | "secrets", 48 | " [ ] 1: Eat more donuts.", 49 | " [ ] 2: Destroy all humans.", 50 | "" 51 | ); 52 | 53 | Execute("add project training"); 54 | Execute("add task training Four Elements of Simple Design"); 55 | Execute("add task training SOLID"); 56 | Execute("add task training Coupling and Cohesion"); 57 | Execute("add task training Primitive Obsession"); 58 | Execute("add task training Outside-In TDD"); 59 | Execute("add task training Interaction-Driven Design"); 60 | 61 | Execute("check 1"); 62 | Execute("check 3"); 63 | Execute("check 5"); 64 | Execute("check 6"); 65 | 66 | Execute("show"); 67 | ReadLines( 68 | "secrets", 69 | " [x] 1: Eat more donuts.", 70 | " [ ] 2: Destroy all humans.", 71 | "", 72 | "training", 73 | " [x] 3: Four Elements of Simple Design", 74 | " [ ] 4: SOLID", 75 | " [x] 5: Coupling and Cohesion", 76 | " [x] 6: Primitive Obsession", 77 | " [ ] 7: Outside-In TDD", 78 | " [ ] 8: Interaction-Driven Design", 79 | "" 80 | ); 81 | 82 | Execute("quit"); 83 | } 84 | 85 | private void Execute(string command) 86 | { 87 | Read(PROMPT); 88 | Write(command); 89 | } 90 | 91 | private void Read(string expectedOutput) 92 | { 93 | var length = expectedOutput.Length; 94 | var actualOutput = console.RetrieveOutput(expectedOutput.Length); 95 | Assert.AreEqual(expectedOutput, actualOutput); 96 | } 97 | 98 | private void ReadLines(params string[] expectedOutput) 99 | { 100 | foreach (var line in expectedOutput) 101 | { 102 | Read(line + Environment.NewLine); 103 | } 104 | } 105 | 106 | private void Write(string input) 107 | { 108 | console.SendInput(input + Environment.NewLine); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /csharp/Tasks.Tests/BlockingStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | 5 | namespace Tasks 6 | { 7 | public class BlockingStream : Stream 8 | { 9 | private readonly Stream underlyingStream; 10 | 11 | public BlockingStream(Stream underlyingStream) 12 | { 13 | this.underlyingStream = underlyingStream; 14 | } 15 | 16 | public override void Flush() 17 | { 18 | lock (underlyingStream) 19 | { 20 | underlyingStream.Flush(); 21 | } 22 | } 23 | 24 | public override int Read(byte[] buffer, int offset, int count) 25 | { 26 | int read = 0; 27 | while (true) 28 | { 29 | lock (underlyingStream) 30 | { 31 | read = underlyingStream.Read(buffer, offset, count); 32 | if (read > 0) 33 | { 34 | return read; 35 | } 36 | } 37 | 38 | Thread.Yield(); 39 | } 40 | } 41 | 42 | public override long Seek(long offset, SeekOrigin origin) 43 | { 44 | lock (underlyingStream) 45 | { 46 | return underlyingStream.Seek(offset, origin); 47 | } 48 | } 49 | 50 | public override void Write(byte[] buffer, int offset, int count) 51 | { 52 | lock (underlyingStream) 53 | { 54 | underlyingStream.Write(buffer, offset, count); 55 | } 56 | } 57 | 58 | public override void SetLength(long value) 59 | { 60 | underlyingStream.SetLength(value); 61 | } 62 | 63 | public override bool CanRead 64 | { 65 | get 66 | { 67 | return underlyingStream.CanRead; 68 | } 69 | } 70 | 71 | public override bool CanSeek 72 | { 73 | get 74 | { 75 | lock (underlyingStream) 76 | { 77 | return underlyingStream.CanSeek; 78 | } 79 | } 80 | } 81 | 82 | public override bool CanWrite 83 | { 84 | get 85 | { 86 | lock (underlyingStream) 87 | { 88 | return underlyingStream.CanWrite; 89 | } 90 | } 91 | } 92 | 93 | public override long Length 94 | { 95 | get 96 | { 97 | lock (underlyingStream) 98 | { 99 | return underlyingStream.Length; 100 | } 101 | } 102 | } 103 | 104 | public override long Position 105 | { 106 | get 107 | { 108 | lock (underlyingStream) 109 | { 110 | return underlyingStream.Position; 111 | } 112 | } 113 | set 114 | { 115 | lock (underlyingStream) 116 | { 117 | underlyingStream.Position = value; 118 | } 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /csharp/Tasks.Tests/FakeConsole.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.IO; 4 | using System.IO.Pipes; 5 | using System.Threading; 6 | 7 | namespace Tasks 8 | { 9 | public class FakeConsole : IConsole 10 | { 11 | private readonly TextReader inputReader; 12 | private readonly TextWriter inputWriter; 13 | 14 | private readonly TextReader outputReader; 15 | private readonly TextWriter outputWriter; 16 | 17 | public FakeConsole() 18 | { 19 | Stream inputStream = new BlockingStream(new ProducerConsumerStream()); 20 | this.inputReader = new StreamReader(inputStream); 21 | this.inputWriter = new StreamWriter(inputStream) { AutoFlush = true }; 22 | 23 | Stream outputStream = new BlockingStream(new ProducerConsumerStream()); 24 | this.outputReader = new StreamReader(outputStream); 25 | this.outputWriter = new StreamWriter(outputStream) { AutoFlush = true }; 26 | } 27 | 28 | public string ReadLine() 29 | { 30 | return inputReader.ReadLine(); 31 | } 32 | 33 | public void Write(string format, params object[] args) 34 | { 35 | outputWriter.Write(format, args); 36 | } 37 | 38 | public void WriteLine(string format, params object[] args) 39 | { 40 | outputWriter.WriteLine(format, args); 41 | } 42 | 43 | public void WriteLine() 44 | { 45 | outputWriter.WriteLine(); 46 | } 47 | 48 | public void SendInput(string input) 49 | { 50 | inputWriter.Write(input); 51 | } 52 | 53 | public string RetrieveOutput(int length) 54 | { 55 | var buffer = new char[length]; 56 | outputReader.ReadBlock(buffer, 0, length); 57 | return new string(buffer); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /csharp/Tasks.Tests/ProducerConsumerStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Tasks 5 | { 6 | class ProducerConsumerStream : Stream 7 | { 8 | private readonly MemoryStream underlyingStream; 9 | private long readPosition = 0; 10 | private long writePosition = 0; 11 | 12 | public ProducerConsumerStream() 13 | { 14 | this.underlyingStream = new MemoryStream(); 15 | } 16 | 17 | public override void Flush() 18 | { 19 | lock (underlyingStream) 20 | { 21 | underlyingStream.Flush(); 22 | } 23 | } 24 | 25 | public override int Read(byte[] buffer, int offset, int count) 26 | { 27 | lock (underlyingStream) 28 | { 29 | underlyingStream.Position = readPosition; 30 | int read = underlyingStream.Read(buffer, offset, count); 31 | readPosition = underlyingStream.Position; 32 | return read; 33 | } 34 | } 35 | 36 | public override void Write(byte[] buffer, int offset, int count) 37 | { 38 | lock (underlyingStream) 39 | { 40 | underlyingStream.Position = writePosition; 41 | underlyingStream.Write(buffer, offset, count); 42 | writePosition = underlyingStream.Position; 43 | } 44 | } 45 | 46 | public override bool CanRead { get { return true; } } 47 | 48 | public override bool CanSeek { get { return false; } } 49 | 50 | public override bool CanWrite { get { return true; } } 51 | 52 | public override long Length 53 | { 54 | get 55 | { 56 | lock (underlyingStream) 57 | { 58 | return underlyingStream.Length; 59 | } 60 | } 61 | } 62 | 63 | public override long Position 64 | { 65 | get { throw new NotSupportedException(); } 66 | set { throw new NotSupportedException(); } 67 | } 68 | 69 | public override long Seek(long offset, SeekOrigin origin) 70 | { 71 | throw new NotSupportedException(); 72 | } 73 | 74 | public override void SetLength(long value) 75 | { 76 | throw new NotImplementedException(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /csharp/Tasks.Tests/Tasks.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /csharp/Tasks/IConsole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Tasks 4 | { 5 | public interface IConsole 6 | { 7 | string ReadLine(); 8 | 9 | void Write(string format, params object[] args); 10 | 11 | void WriteLine(string format, params object[] args); 12 | 13 | void WriteLine(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /csharp/Tasks/RealConsole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Tasks 4 | { 5 | public class RealConsole : IConsole 6 | { 7 | public string ReadLine() 8 | { 9 | return Console.ReadLine(); 10 | } 11 | 12 | public void Write(string format, params object[] args) 13 | { 14 | Console.Write(format, args); 15 | } 16 | 17 | public void WriteLine(string format, params object[] args) 18 | { 19 | Console.WriteLine(format, args); 20 | } 21 | 22 | public void WriteLine() 23 | { 24 | Console.WriteLine(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /csharp/Tasks/Task.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Tasks 5 | { 6 | public class Task 7 | { 8 | public long Id { get; set; } 9 | 10 | public string Description { get; set; } 11 | 12 | public bool Done { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /csharp/Tasks/TaskList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Tasks 6 | { 7 | public sealed class TaskList 8 | { 9 | private const string QUIT = "quit"; 10 | 11 | private readonly IDictionary> tasks = new Dictionary>(); 12 | private readonly IConsole console; 13 | 14 | private long lastId = 0; 15 | 16 | public static void Main(string[] args) 17 | { 18 | new TaskList(new RealConsole()).Run(); 19 | } 20 | 21 | public TaskList(IConsole console) 22 | { 23 | this.console = console; 24 | } 25 | 26 | public void Run() 27 | { 28 | while (true) { 29 | console.Write("> "); 30 | var command = console.ReadLine(); 31 | if (command == QUIT) { 32 | break; 33 | } 34 | Execute(command); 35 | } 36 | } 37 | 38 | private void Execute(string commandLine) 39 | { 40 | var commandRest = commandLine.Split(" ".ToCharArray(), 2); 41 | var command = commandRest[0]; 42 | switch (command) { 43 | case "show": 44 | Show(); 45 | break; 46 | case "add": 47 | Add(commandRest[1]); 48 | break; 49 | case "check": 50 | Check(commandRest[1]); 51 | break; 52 | case "uncheck": 53 | Uncheck(commandRest[1]); 54 | break; 55 | case "help": 56 | Help(); 57 | break; 58 | default: 59 | Error(command); 60 | break; 61 | } 62 | } 63 | 64 | private void Show() 65 | { 66 | foreach (var project in tasks) { 67 | console.WriteLine(project.Key); 68 | foreach (var task in project.Value) { 69 | console.WriteLine(" [{0}] {1}: {2}", (task.Done ? 'x' : ' '), task.Id, task.Description); 70 | } 71 | console.WriteLine(); 72 | } 73 | } 74 | 75 | private void Add(string commandLine) 76 | { 77 | var subcommandRest = commandLine.Split(" ".ToCharArray(), 2); 78 | var subcommand = subcommandRest[0]; 79 | if (subcommand == "project") { 80 | AddProject(subcommandRest[1]); 81 | } else if (subcommand == "task") { 82 | var projectTask = subcommandRest[1].Split(" ".ToCharArray(), 2); 83 | AddTask(projectTask[0], projectTask[1]); 84 | } 85 | } 86 | 87 | private void AddProject(string name) 88 | { 89 | tasks[name] = new List(); 90 | } 91 | 92 | private void AddTask(string project, string description) 93 | { 94 | if (!tasks.TryGetValue(project, out IList projectTasks)) 95 | { 96 | Console.WriteLine("Could not find a project with the name \"{0}\".", project); 97 | return; 98 | } 99 | projectTasks.Add(new Task { Id = NextId(), Description = description, Done = false }); 100 | } 101 | 102 | private void Check(string idString) 103 | { 104 | SetDone(idString, true); 105 | } 106 | 107 | private void Uncheck(string idString) 108 | { 109 | SetDone(idString, false); 110 | } 111 | 112 | private void SetDone(string idString, bool done) 113 | { 114 | int id = int.Parse(idString); 115 | var identifiedTask = tasks 116 | .Select(project => project.Value.FirstOrDefault(task => task.Id == id)) 117 | .Where(task => task != null) 118 | .FirstOrDefault(); 119 | if (identifiedTask == null) { 120 | console.WriteLine("Could not find a task with an ID of {0}.", id); 121 | return; 122 | } 123 | 124 | identifiedTask.Done = done; 125 | } 126 | 127 | private void Help() 128 | { 129 | console.WriteLine("Commands:"); 130 | console.WriteLine(" show"); 131 | console.WriteLine(" add project "); 132 | console.WriteLine(" add task "); 133 | console.WriteLine(" check "); 134 | console.WriteLine(" uncheck "); 135 | console.WriteLine(); 136 | } 137 | 138 | private void Error(string command) 139 | { 140 | console.WriteLine("I don't know what the command \"{0}\" is.", command); 141 | } 142 | 143 | private long NextId() 144 | { 145 | return ++lastId; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /csharp/Tasks/Tasks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | 5 | Exe 6 | Tasks.TaskList 7 | 8 | 9 | -------------------------------------------------------------------------------- /golang/.gitignore: -------------------------------------------------------------------------------- 1 | # temp files 2 | *~ 3 | *.orig 4 | 5 | # binaries 6 | /task 7 | /bin/* 8 | -------------------------------------------------------------------------------- /golang/README.md: -------------------------------------------------------------------------------- 1 | # Golang code 2 | 3 | ## Usage 4 | 5 | Only a `go` binary is required. Get it through your distribution repositories or on the [official Golang site](https://golang.org/dl/). 6 | 7 | #### Run the tests 8 | 9 | ```sh 10 | > ./run_tests.sh [-v] 11 | ``` 12 | 13 | (Calls `go test` after setting up `GOPATH`) 14 | 15 | #### Run the application 16 | 17 | ```sh 18 | > ./build_and_run.sh 19 | ``` 20 | 21 | (Calls `go build` after setting up `GOPATH`) 22 | 23 | ## Notes on testing 24 | 25 | The main scenario test in `main_test.go` writes to the input descriptor 26 | of `TaskList.Run()`, and reads from its output descriptor, through 27 | [`io.Pipe`](https://golang.org/pkg/io/#Pipe). 28 | 29 | Writes and reads in pipes are blocking if the other side is not ready, 30 | as a consequence, a missing read (invalid number of lines in the output, 31 | for example) will lead to a deadlock. 32 | 33 | Similarly, the scenario expects the `TaskList.Run()` to finish (reception of the 34 | `quit` command), if it is forgotten another deadlock will be detected. 35 | 36 | In the case of deadlocks, test output will be unusable: to help with debugging, 37 | current scenario steps are logged on standard output and displayed when using 38 | `./run_tests.sh -v`. 39 | -------------------------------------------------------------------------------- /golang/build_and_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd "$(dirname "$0")" > /dev/null 4 | 5 | GOPATH="$PWD":"$GOPATH" go build -o bin/task task && bin/task 6 | 7 | popd > /dev/null 8 | -------------------------------------------------------------------------------- /golang/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd "$(dirname "$0")" > /dev/null 4 | 5 | GOPATH="$PWD":"$GOPATH" go test ./... $@ 6 | 7 | popd > /dev/null 8 | -------------------------------------------------------------------------------- /golang/src/task/list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | /* 13 | * Features to add 14 | * 15 | * 1. Deadlines 16 | * (i) Give each task an optional deadline with the 'deadline ' command. 17 | * (ii) Show all tasks due today with the 'today' command. 18 | * 2. Customisable IDs 19 | * (i) Allow the user to specify an identifier that's not a number. 20 | * (ii) Disallow spaces and special characters from the ID. 21 | * 3. Deletion 22 | * (i) Allow users to delete tasks with the 'delete ' command. 23 | * 4. Views 24 | * (i) View tasks by date with the 'view by date' command. 25 | * (ii) View tasks by deadline with the 'view by deadline' command. 26 | * (iii) Don't remove the functionality that allows users to view tasks by project, 27 | * but change the command to 'view by project' 28 | */ 29 | 30 | const ( 31 | // Quit is the text command used to quit the task manager. 32 | Quit string = "quit" 33 | prompt string = "> " 34 | ) 35 | 36 | // TaskList is a set of tasks, grouped by project. 37 | type TaskList struct { 38 | in io.Reader 39 | out io.Writer 40 | 41 | projectTasks map[string][]*Task 42 | lastID int64 43 | } 44 | 45 | // NewTaskList initializes a TaskList on the given I/O descriptors. 46 | func NewTaskList(in io.Reader, out io.Writer) *TaskList { 47 | return &TaskList{ 48 | in: in, 49 | out: out, 50 | projectTasks: make(map[string][]*Task), 51 | lastID: 0, 52 | } 53 | } 54 | 55 | // Run runs the command loop of the task manager. 56 | // Sequentially executes any given command, until the user types the Quit message. 57 | func (l *TaskList) Run() { 58 | scanner := bufio.NewScanner(l.in) 59 | 60 | fmt.Fprint(l.out, prompt) 61 | for scanner.Scan() { 62 | cmdLine := scanner.Text() 63 | if cmdLine == Quit { 64 | return 65 | } 66 | 67 | l.execute(cmdLine) 68 | fmt.Fprint(l.out, prompt) 69 | } 70 | } 71 | 72 | func (l *TaskList) execute(cmdLine string) { 73 | args := strings.Split(cmdLine, " ") 74 | command := args[0] 75 | switch command { 76 | case "show": 77 | l.show() 78 | case "add": 79 | l.add(args[1:]) 80 | case "check": 81 | l.check(args[1]) 82 | case "uncheck": 83 | l.uncheck(args[1]) 84 | case "help": 85 | l.help() 86 | default: 87 | l.error(command) 88 | } 89 | } 90 | 91 | func (l *TaskList) help() { 92 | fmt.Fprintln(l.out, `Commands: 93 | show 94 | add project 95 | add task 96 | check 97 | uncheck 98 | `) 99 | } 100 | 101 | func (l *TaskList) error(command string) { 102 | fmt.Fprintf(l.out, "Unknown command \"%s\".\n", command) 103 | } 104 | 105 | func (l *TaskList) show() { 106 | // sort projects (to make output deterministic) 107 | sortedProjects := make([]string, 0, len(l.projectTasks)) 108 | for project := range l.projectTasks { 109 | sortedProjects = append(sortedProjects, project) 110 | } 111 | sort.Sort(sort.StringSlice(sortedProjects)) 112 | 113 | // show projects sequentially 114 | for _, project := range sortedProjects { 115 | tasks := l.projectTasks[project] 116 | fmt.Fprintf(l.out, "%s\n", project) 117 | for _, task := range tasks { 118 | done := ' ' 119 | if task.IsDone() { 120 | done = 'X' 121 | } 122 | fmt.Fprintf(l.out, " [%c] %d: %s\n", done, task.GetID(), task.GetDescription()) 123 | } 124 | fmt.Fprintln(l.out) 125 | } 126 | } 127 | 128 | func (l *TaskList) add(args []string) { 129 | if len(args) < 2 { 130 | fmt.Fprintln(l.out, "Missing parameters for \"add\" command.") 131 | return 132 | } 133 | projectName := args[1] 134 | if args[0] == "project" { 135 | l.addProject(projectName) 136 | } else if args[0] == "task" { 137 | description := strings.Join(args[2:], " ") 138 | l.addTask(projectName, description) 139 | } 140 | } 141 | 142 | func (l *TaskList) addProject(name string) { 143 | l.projectTasks[name] = make([]*Task, 0) 144 | } 145 | 146 | func (l *TaskList) addTask(projectName, description string) { 147 | tasks, ok := l.projectTasks[projectName] 148 | if !ok { 149 | fmt.Fprintf(l.out, "Could not find a project with the name \"%s\".\n", projectName) 150 | return 151 | } 152 | l.projectTasks[projectName] = append(tasks, NewTask(l.nextID(), description, false)) 153 | } 154 | 155 | func (l *TaskList) check(idString string) { 156 | l.setDone(idString, true) 157 | } 158 | 159 | func (l *TaskList) uncheck(idString string) { 160 | l.setDone(idString, false) 161 | } 162 | 163 | func (l *TaskList) setDone(idString string, done bool) { 164 | id, err := strconv.ParseInt(idString, 10, 64) 165 | if err != nil { 166 | fmt.Fprintf(l.out, "Invalid ID \"%s\".\n", idString) 167 | return 168 | } 169 | 170 | for _, tasks := range l.projectTasks { 171 | for _, task := range tasks { 172 | if task.GetID() == id { 173 | task.SetDone(done) 174 | return 175 | } 176 | } 177 | } 178 | 179 | fmt.Fprintf(l.out, "Task with ID \"%d\" not found.\n", id) 180 | } 181 | 182 | func (l *TaskList) nextID() int64 { 183 | l.lastID++ 184 | return l.lastID 185 | } 186 | -------------------------------------------------------------------------------- /golang/src/task/main.go: -------------------------------------------------------------------------------- 1 | // Package main implements a command-line task manager. 2 | // A manager is a TaskList object, which is started with the Run() function 3 | // and then scans and executes user commands. 4 | package main 5 | 6 | import "os" 7 | 8 | func main() { 9 | NewTaskList(os.Stdin, os.Stdout).Run() 10 | } 11 | -------------------------------------------------------------------------------- /golang/src/task/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "sync" 8 | "testing" 9 | ) 10 | 11 | type scenarioTester struct { 12 | *testing.T 13 | 14 | inWriter io.Writer 15 | outReader io.Reader 16 | outScanner *bufio.Scanner 17 | } 18 | 19 | func TestRun(t *testing.T) { 20 | // setup input/output 21 | inPR, inPW := io.Pipe() 22 | defer inPR.Close() 23 | outPR, outPW := io.Pipe() 24 | defer outPR.Close() 25 | tester := &scenarioTester{ 26 | T: t, 27 | inWriter: inPW, 28 | outReader: outPR, 29 | outScanner: bufio.NewScanner(outPR), 30 | } 31 | 32 | // run main program 33 | var wg sync.WaitGroup 34 | go func() { 35 | wg.Add(1) 36 | NewTaskList(inPR, outPW).Run() 37 | outPW.Close() 38 | wg.Done() 39 | }() 40 | 41 | // run command-line scenario 42 | fmt.Println("(show empty)") 43 | tester.execute("show") 44 | 45 | fmt.Println("(add project)") 46 | tester.execute("add project secrets") 47 | fmt.Println("(add tasks)") 48 | tester.execute("add task secrets Eat more donuts.") 49 | tester.execute("add task secrets Destroy all humans.") 50 | 51 | fmt.Println("(show tasks)") 52 | tester.execute("show") 53 | tester.readLines([]string{ 54 | "secrets", 55 | " [ ] 1: Eat more donuts.", 56 | " [ ] 2: Destroy all humans.", 57 | "", 58 | }) 59 | 60 | fmt.Println("(add second project)") 61 | tester.execute("add project training") 62 | fmt.Println("(add more tasks)") 63 | tester.execute("add task training Four Elements of Simple Design") 64 | tester.execute("add task training SOLID") 65 | tester.execute("add task training Coupling and Cohesion") 66 | tester.execute("add task training Primitive Obsession") 67 | tester.execute("add task training Outside-In TDD") 68 | tester.execute("add task training Interaction-Driven Design") 69 | 70 | fmt.Println("(check tasks)") 71 | tester.execute("check 1") 72 | tester.execute("check 3") 73 | tester.execute("check 5") 74 | tester.execute("check 6") 75 | 76 | fmt.Println("(show completed tasks)") 77 | tester.execute("show") 78 | tester.readLines([]string{ 79 | "secrets", 80 | " [X] 1: Eat more donuts.", 81 | " [ ] 2: Destroy all humans.", 82 | "", 83 | "training", 84 | " [X] 3: Four Elements of Simple Design", 85 | " [ ] 4: SOLID", 86 | " [X] 5: Coupling and Cohesion", 87 | " [X] 6: Primitive Obsession", 88 | " [ ] 7: Outside-In TDD", 89 | " [ ] 8: Interaction-Driven Design", 90 | "", 91 | }) 92 | 93 | fmt.Println("(quit)") 94 | tester.execute("quit") 95 | 96 | // make sure main program has quit 97 | inPW.Close() 98 | wg.Wait() 99 | } 100 | 101 | // execute calls a command, by writing it into the scenario writer. 102 | // It first reads the command prompt, then sends the command. 103 | func (t *scenarioTester) execute(cmd string) { 104 | p := make([]byte, len(prompt)) 105 | _, err := t.outReader.Read(p) 106 | if err != nil { 107 | t.Errorf("Prompt could not be read: %v", err) 108 | return 109 | } 110 | if string(p) != prompt { 111 | t.Errorf("Invalid prompt, expected \"%s\", got \"%s\"", prompt, string(p)) 112 | return 113 | } 114 | // send command 115 | fmt.Fprintln(t.inWriter, cmd) 116 | } 117 | 118 | // readLines reads lines from the scenario scanner, making sure they match 119 | // the expected given lines. 120 | // In case it fails or does not match, makes the calling test fail. 121 | func (t *scenarioTester) readLines(lines []string) { 122 | for _, expected := range lines { 123 | if !t.outScanner.Scan() { 124 | t.Errorf("Expected \"%s\", no input found", expected) 125 | break 126 | } 127 | actual := t.outScanner.Text() 128 | if actual != expected { 129 | t.Errorf("Expected \"%s\", got \"%s\"", expected, actual) 130 | } 131 | } 132 | if err := t.outScanner.Err(); err != nil { 133 | t.Fatalf("Could not read input: %v", err) 134 | } 135 | } 136 | 137 | // discardLines reads lines from the scenario scanner, and drops them. 138 | // Used to empty buffers. 139 | func (t *scenarioTester) discardLines(n int) { 140 | for i := 0; i < n; i++ { 141 | if !t.outScanner.Scan() { 142 | t.Error("Expected a line, no input found") 143 | break 144 | } 145 | } 146 | if err := t.outScanner.Err(); err != nil { 147 | t.Fatalf("Could not read input: %v", err) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /golang/src/task/task.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Task describes an elementary task. 4 | type Task struct { 5 | id int64 6 | description string 7 | done bool 8 | } 9 | 10 | // NewTask initializes a Task with the given ID, description and completion status. 11 | func NewTask(id int64, description string, done bool) *Task { 12 | return &Task{ 13 | id: id, 14 | description: description, 15 | done: done, 16 | } 17 | } 18 | 19 | // GetID returns the task ID. 20 | func (t *Task) GetID() int64 { 21 | return t.id 22 | } 23 | 24 | // GetDescription returns the task description. 25 | func (t *Task) GetDescription() string { 26 | return t.description 27 | } 28 | 29 | // IsDone returns whether the task is done or not. 30 | func (t *Task) IsDone() bool { 31 | return t.done 32 | } 33 | 34 | // SetDone changes the completion status of the task. 35 | func (t *Task) SetDone(done bool) { 36 | t.done = done 37 | } 38 | -------------------------------------------------------------------------------- /java/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.iml 3 | -------------------------------------------------------------------------------- /java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | com.codurance.training 5 | tasks 6 | 0.1 7 | 8 | Task List 9 | 10 | 11 | 12 | junit 13 | junit 14 | 4.13.1 15 | test 16 | 17 | 18 | org.hamcrest 19 | java-hamcrest 20 | 2.0.0.0 21 | test 22 | 23 | 24 | 25 | 26 | 27 | 28 | org.apache.maven.plugins 29 | maven-compiler-plugin 30 | 2.5.1 31 | 32 | 1.8 33 | 1.8 34 | 35 | 36 | 37 | org.codehaus.mojo 38 | appassembler-maven-plugin 39 | 1.8 40 | 41 | 42 | 43 | task-list 44 | com.codurance.training.tasks.TaskList 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /java/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "${BASH_SOURCE[0]}")" 6 | 7 | mvn package appassembler:assemble 8 | echo 9 | echo 10 | echo 11 | sh ./target/appassembler/bin/task-list 12 | -------------------------------------------------------------------------------- /java/src/main/java/com/codurance/training/tasks/Task.java: -------------------------------------------------------------------------------- 1 | package com.codurance.training.tasks; 2 | 3 | public final class Task { 4 | private final long id; 5 | private final String description; 6 | private boolean done; 7 | 8 | public Task(long id, String description, boolean done) { 9 | this.id = id; 10 | this.description = description; 11 | this.done = done; 12 | } 13 | 14 | public long getId() { 15 | return id; 16 | } 17 | 18 | public String getDescription() { 19 | return description; 20 | } 21 | 22 | public boolean isDone() { 23 | return done; 24 | } 25 | 26 | public void setDone(boolean done) { 27 | this.done = done; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /java/src/main/java/com/codurance/training/tasks/TaskList.java: -------------------------------------------------------------------------------- 1 | package com.codurance.training.tasks; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.io.PrintWriter; 7 | import java.util.ArrayList; 8 | import java.util.LinkedHashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public final class TaskList implements Runnable { 13 | private static final String QUIT = "quit"; 14 | 15 | private final Map> tasks = new LinkedHashMap<>(); 16 | private final BufferedReader in; 17 | private final PrintWriter out; 18 | 19 | private long lastId = 0; 20 | 21 | public static void main(String[] args) throws Exception { 22 | BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 23 | PrintWriter out = new PrintWriter(System.out); 24 | new TaskList(in, out).run(); 25 | } 26 | 27 | public TaskList(BufferedReader reader, PrintWriter writer) { 28 | this.in = reader; 29 | this.out = writer; 30 | } 31 | 32 | public void run() { 33 | while (true) { 34 | out.print("> "); 35 | out.flush(); 36 | String command; 37 | try { 38 | command = in.readLine(); 39 | } catch (IOException e) { 40 | throw new RuntimeException(e); 41 | } 42 | if (command.equals(QUIT)) { 43 | break; 44 | } 45 | execute(command); 46 | } 47 | } 48 | 49 | private void execute(String commandLine) { 50 | String[] commandRest = commandLine.split(" ", 2); 51 | String command = commandRest[0]; 52 | switch (command) { 53 | case "show": 54 | show(); 55 | break; 56 | case "add": 57 | add(commandRest[1]); 58 | break; 59 | case "check": 60 | check(commandRest[1]); 61 | break; 62 | case "uncheck": 63 | uncheck(commandRest[1]); 64 | break; 65 | case "help": 66 | help(); 67 | break; 68 | default: 69 | error(command); 70 | break; 71 | } 72 | } 73 | 74 | private void show() { 75 | for (Map.Entry> project : tasks.entrySet()) { 76 | out.println(project.getKey()); 77 | for (Task task : project.getValue()) { 78 | out.printf(" [%c] %d: %s%n", (task.isDone() ? 'x' : ' '), task.getId(), task.getDescription()); 79 | } 80 | out.println(); 81 | } 82 | } 83 | 84 | private void add(String commandLine) { 85 | String[] subcommandRest = commandLine.split(" ", 2); 86 | String subcommand = subcommandRest[0]; 87 | if (subcommand.equals("project")) { 88 | addProject(subcommandRest[1]); 89 | } else if (subcommand.equals("task")) { 90 | String[] projectTask = subcommandRest[1].split(" ", 2); 91 | addTask(projectTask[0], projectTask[1]); 92 | } 93 | } 94 | 95 | private void addProject(String name) { 96 | tasks.put(name, new ArrayList()); 97 | } 98 | 99 | private void addTask(String project, String description) { 100 | List projectTasks = tasks.get(project); 101 | if (projectTasks == null) { 102 | out.printf("Could not find a project with the name \"%s\".", project); 103 | out.println(); 104 | return; 105 | } 106 | projectTasks.add(new Task(nextId(), description, false)); 107 | } 108 | 109 | private void check(String idString) { 110 | setDone(idString, true); 111 | } 112 | 113 | private void uncheck(String idString) { 114 | setDone(idString, false); 115 | } 116 | 117 | private void setDone(String idString, boolean done) { 118 | int id = Integer.parseInt(idString); 119 | for (Map.Entry> project : tasks.entrySet()) { 120 | for (Task task : project.getValue()) { 121 | if (task.getId() == id) { 122 | task.setDone(done); 123 | return; 124 | } 125 | } 126 | } 127 | out.printf("Could not find a task with an ID of %d.", id); 128 | out.println(); 129 | } 130 | 131 | private void help() { 132 | out.println("Commands:"); 133 | out.println(" show"); 134 | out.println(" add project "); 135 | out.println(" add task "); 136 | out.println(" check "); 137 | out.println(" uncheck "); 138 | out.println(); 139 | } 140 | 141 | private void error(String command) { 142 | out.printf("I don't know what the command \"%s\" is.", command); 143 | out.println(); 144 | } 145 | 146 | private long nextId() { 147 | return ++lastId; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /java/src/test/java/com/codurance/training/tasks/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.codurance.training.tasks; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.io.PipedInputStream; 7 | import java.io.PipedOutputStream; 8 | import java.io.PrintWriter; 9 | import org.junit.After; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | 13 | import static java.lang.System.lineSeparator; 14 | import static org.hamcrest.MatcherAssert.assertThat; 15 | import static org.hamcrest.Matchers.is; 16 | 17 | public final class ApplicationTest { 18 | public static final String PROMPT = "> "; 19 | private final PipedOutputStream inStream = new PipedOutputStream(); 20 | private final PrintWriter inWriter = new PrintWriter(inStream, true); 21 | 22 | private final PipedInputStream outStream = new PipedInputStream(); 23 | private final BufferedReader outReader = new BufferedReader(new InputStreamReader(outStream)); 24 | 25 | private Thread applicationThread; 26 | 27 | public ApplicationTest() throws IOException { 28 | BufferedReader in = new BufferedReader(new InputStreamReader(new PipedInputStream(inStream))); 29 | PrintWriter out = new PrintWriter(new PipedOutputStream(outStream), true); 30 | TaskList taskList = new TaskList(in, out); 31 | applicationThread = new Thread(taskList); 32 | } 33 | 34 | @Before public void 35 | start_the_application() { 36 | applicationThread.start(); 37 | } 38 | 39 | @After public void 40 | kill_the_application() throws IOException, InterruptedException { 41 | if (!stillRunning()) { 42 | return; 43 | } 44 | 45 | Thread.sleep(1000); 46 | if (!stillRunning()) { 47 | return; 48 | } 49 | 50 | applicationThread.interrupt(); 51 | throw new IllegalStateException("The application is still running."); 52 | } 53 | 54 | @Test(timeout = 1000) public void 55 | it_works() throws IOException { 56 | execute("show"); 57 | 58 | execute("add project secrets"); 59 | execute("add task secrets Eat more donuts."); 60 | execute("add task secrets Destroy all humans."); 61 | 62 | execute("show"); 63 | readLines( 64 | "secrets", 65 | " [ ] 1: Eat more donuts.", 66 | " [ ] 2: Destroy all humans.", 67 | "" 68 | ); 69 | 70 | execute("add project training"); 71 | execute("add task training Four Elements of Simple Design"); 72 | execute("add task training SOLID"); 73 | execute("add task training Coupling and Cohesion"); 74 | execute("add task training Primitive Obsession"); 75 | execute("add task training Outside-In TDD"); 76 | execute("add task training Interaction-Driven Design"); 77 | 78 | execute("check 1"); 79 | execute("check 3"); 80 | execute("check 5"); 81 | execute("check 6"); 82 | 83 | execute("show"); 84 | readLines( 85 | "secrets", 86 | " [x] 1: Eat more donuts.", 87 | " [ ] 2: Destroy all humans.", 88 | "", 89 | "training", 90 | " [x] 3: Four Elements of Simple Design", 91 | " [ ] 4: SOLID", 92 | " [x] 5: Coupling and Cohesion", 93 | " [x] 6: Primitive Obsession", 94 | " [ ] 7: Outside-In TDD", 95 | " [ ] 8: Interaction-Driven Design", 96 | "" 97 | ); 98 | 99 | execute("quit"); 100 | } 101 | 102 | private void execute(String command) throws IOException { 103 | read(PROMPT); 104 | write(command); 105 | } 106 | 107 | private void read(String expectedOutput) throws IOException { 108 | int length = expectedOutput.length(); 109 | char[] buffer = new char[length]; 110 | outReader.read(buffer, 0, length); 111 | assertThat(String.valueOf(buffer), is(expectedOutput)); 112 | } 113 | 114 | private void readLines(String... expectedOutput) throws IOException { 115 | for (String line : expectedOutput) { 116 | read(line + lineSeparator()); 117 | } 118 | } 119 | 120 | private void write(String input) { 121 | inWriter.println(input); 122 | } 123 | 124 | private boolean stillRunning() { 125 | return applicationThread != null && applicationThread.isAlive(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /kotlin/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.iml 3 | .idea/ 4 | -------------------------------------------------------------------------------- /kotlin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | com.codurance.training 5 | tasks 6 | 0.1 7 | 8 | Task List 9 | 10 | 11 | 1.6.0 12 | 13 | 14 | 15 | 16 | junit 17 | junit 18 | 4.13.1 19 | test 20 | 21 | 22 | org.hamcrest 23 | java-hamcrest 24 | 2.0.0.0 25 | test 26 | 27 | 28 | org.jetbrains.kotlin 29 | kotlin-stdlib 30 | ${kotlin.version} 31 | 32 | 33 | org.jetbrains.kotlin 34 | kotlin-test 35 | ${kotlin.version} 36 | test 37 | 38 | 39 | 40 | 41 | 42 | 43 | org.codehaus.mojo 44 | appassembler-maven-plugin 45 | 1.8 46 | 47 | 48 | 49 | task-list 50 | com.codurance.training.tasks.TaskList 51 | 52 | 53 | 54 | 55 | 56 | org.jetbrains.kotlin 57 | kotlin-maven-plugin 58 | ${kotlin.version} 59 | 60 | 61 | compile 62 | compile 63 | 64 | compile 65 | 66 | 67 | 68 | test-compile 69 | test-compile 70 | 71 | test-compile 72 | 73 | 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-compiler-plugin 79 | 2.5.1 80 | 81 | 82 | compile 83 | compile 84 | 85 | compile 86 | 87 | 88 | 89 | testCompile 90 | test-compile 91 | 92 | testCompile 93 | 94 | 95 | 96 | 97 | 1.8 98 | 1.8 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /kotlin/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "${BASH_SOURCE[0]}")" 6 | 7 | mvn package appassembler:assemble 8 | echo 9 | echo 10 | echo 11 | sh ./target/appassembler/bin/task-list 12 | -------------------------------------------------------------------------------- /kotlin/src/main/java/com/codurance/training/tasks/Task.kt: -------------------------------------------------------------------------------- 1 | package com.codurance.training.tasks 2 | 3 | class Task(val id: Long, val description: String, var isDone: Boolean) 4 | -------------------------------------------------------------------------------- /kotlin/src/main/java/com/codurance/training/tasks/TaskList.kt: -------------------------------------------------------------------------------- 1 | package com.codurance.training.tasks 2 | 3 | import java.io.BufferedReader 4 | import java.io.IOException 5 | import java.io.InputStreamReader 6 | import java.io.PrintWriter 7 | import java.util.* 8 | 9 | class TaskList(private val `in`: BufferedReader, private val out: PrintWriter) : Runnable { 10 | 11 | private val tasks = LinkedHashMap>() 12 | 13 | private var lastId: Long = 0 14 | 15 | override fun run() { 16 | while (true) { 17 | out.print("> ") 18 | out.flush() 19 | val command: String 20 | try { 21 | command = `in`.readLine() 22 | } catch (e: IOException) { 23 | throw RuntimeException(e) 24 | } 25 | 26 | if (command == QUIT) { 27 | break 28 | } 29 | execute(command) 30 | } 31 | } 32 | 33 | private fun execute(commandLine: String) { 34 | val commandRest = commandLine.split(" ".toRegex(), 2).toTypedArray() 35 | val command = commandRest[0] 36 | when (command) { 37 | "show" -> show() 38 | "add" -> add(commandRest[1]) 39 | "check" -> check(commandRest[1]) 40 | "uncheck" -> uncheck(commandRest[1]) 41 | "help" -> help() 42 | else -> error(command) 43 | } 44 | } 45 | 46 | private fun show() { 47 | for ((key, value) in tasks) { 48 | out.println(key) 49 | for (task in value) { 50 | out.printf(" [%c] %d: %s%n", if (task.isDone) 'x' else ' ', task.id, task.description) 51 | } 52 | out.println() 53 | } 54 | } 55 | 56 | private fun add(commandLine: String) { 57 | val subcommandRest = commandLine.split(" ".toRegex(), 2).toTypedArray() 58 | val subcommand = subcommandRest[0] 59 | if (subcommand == "project") { 60 | addProject(subcommandRest[1]) 61 | } else if (subcommand == "task") { 62 | val projectTask = subcommandRest[1].split(" ".toRegex(), 2).toTypedArray() 63 | addTask(projectTask[0], projectTask[1]) 64 | } 65 | } 66 | 67 | private fun addProject(name: String) { 68 | tasks[name] = ArrayList() 69 | } 70 | 71 | private fun addTask(project: String, description: String) { 72 | val projectTasks = tasks[project] 73 | if (projectTasks == null) { 74 | out.printf("Could not find a project with the name \"%s\".", project) 75 | out.println() 76 | return 77 | } 78 | projectTasks.add(Task(nextId(), description, false)) 79 | } 80 | 81 | private fun check(idString: String) { 82 | setDone(idString, true) 83 | } 84 | 85 | private fun uncheck(idString: String) { 86 | setDone(idString, false) 87 | } 88 | 89 | private fun setDone(idString: String, done: Boolean) { 90 | val id = Integer.parseInt(idString) 91 | for ((_, value) in tasks) { 92 | for (task in value) { 93 | if (task.id == id.toLong()) { 94 | task.isDone = done 95 | return 96 | } 97 | } 98 | } 99 | out.printf("Could not find a task with an ID of %d.", id) 100 | out.println() 101 | } 102 | 103 | private fun help() { 104 | out.println("Commands:") 105 | out.println(" show") 106 | out.println(" add project ") 107 | out.println(" add task ") 108 | out.println(" check ") 109 | out.println(" uncheck ") 110 | out.println() 111 | } 112 | 113 | private fun error(command: String) { 114 | out.printf("I don't know what the command \"%s\" is.", command) 115 | out.println() 116 | } 117 | 118 | private fun nextId(): Long { 119 | return ++lastId 120 | } 121 | 122 | companion object { 123 | private val QUIT = "quit" 124 | 125 | @Throws(Exception::class) 126 | @JvmStatic 127 | fun main(args: Array) { 128 | val `in` = BufferedReader(InputStreamReader(System.`in`)) 129 | val out = PrintWriter(System.out) 130 | TaskList(`in`, out).run() 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /kotlin/src/test/java/com/codurance/training/tasks/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.codurance.training.tasks; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.io.PipedInputStream; 7 | import java.io.PipedOutputStream; 8 | import java.io.PrintWriter; 9 | import org.junit.After; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | 13 | import static java.lang.System.lineSeparator; 14 | import static org.hamcrest.MatcherAssert.assertThat; 15 | import static org.hamcrest.Matchers.is; 16 | 17 | public final class ApplicationTest { 18 | public static final String PROMPT = "> "; 19 | private final PipedOutputStream inStream = new PipedOutputStream(); 20 | private final PrintWriter inWriter = new PrintWriter(inStream, true); 21 | 22 | private final PipedInputStream outStream = new PipedInputStream(); 23 | private final BufferedReader outReader = new BufferedReader(new InputStreamReader(outStream)); 24 | 25 | private Thread applicationThread; 26 | 27 | public ApplicationTest() throws IOException { 28 | BufferedReader in = new BufferedReader(new InputStreamReader(new PipedInputStream(inStream))); 29 | PrintWriter out = new PrintWriter(new PipedOutputStream(outStream), true); 30 | TaskList taskList = new TaskList(in, out); 31 | applicationThread = new Thread(taskList); 32 | } 33 | 34 | @Before public void 35 | start_the_application() { 36 | applicationThread.start(); 37 | } 38 | 39 | @After public void 40 | kill_the_application() throws IOException, InterruptedException { 41 | if (!stillRunning()) { 42 | return; 43 | } 44 | 45 | Thread.sleep(1000); 46 | if (!stillRunning()) { 47 | return; 48 | } 49 | 50 | applicationThread.interrupt(); 51 | throw new IllegalStateException("The application is still running."); 52 | } 53 | 54 | @Test(timeout = 1000) public void 55 | it_works() throws IOException { 56 | execute("show"); 57 | 58 | execute("add project secrets"); 59 | execute("add task secrets Eat more donuts."); 60 | execute("add task secrets Destroy all humans."); 61 | 62 | execute("show"); 63 | readLines( 64 | "secrets", 65 | " [ ] 1: Eat more donuts.", 66 | " [ ] 2: Destroy all humans.", 67 | "" 68 | ); 69 | 70 | execute("add project training"); 71 | execute("add task training Four Elements of Simple Design"); 72 | execute("add task training SOLID"); 73 | execute("add task training Coupling and Cohesion"); 74 | execute("add task training Primitive Obsession"); 75 | execute("add task training Outside-In TDD"); 76 | execute("add task training Interaction-Driven Design"); 77 | 78 | execute("check 1"); 79 | execute("check 3"); 80 | execute("check 5"); 81 | execute("check 6"); 82 | 83 | execute("show"); 84 | readLines( 85 | "secrets", 86 | " [x] 1: Eat more donuts.", 87 | " [ ] 2: Destroy all humans.", 88 | "", 89 | "training", 90 | " [x] 3: Four Elements of Simple Design", 91 | " [ ] 4: SOLID", 92 | " [x] 5: Coupling and Cohesion", 93 | " [x] 6: Primitive Obsession", 94 | " [ ] 7: Outside-In TDD", 95 | " [ ] 8: Interaction-Driven Design", 96 | "" 97 | ); 98 | 99 | execute("quit"); 100 | } 101 | 102 | private void execute(String command) throws IOException { 103 | read(PROMPT); 104 | write(command); 105 | } 106 | 107 | private void read(String expectedOutput) throws IOException { 108 | int length = expectedOutput.length(); 109 | char[] buffer = new char[length]; 110 | outReader.read(buffer, 0, length); 111 | assertThat(String.valueOf(buffer), is(expectedOutput)); 112 | } 113 | 114 | private void readLines(String... expectedOutput) throws IOException { 115 | for (String line : expectedOutput) { 116 | read(line + lineSeparator()); 117 | } 118 | } 119 | 120 | private void write(String input) { 121 | inWriter.println(input); 122 | } 123 | 124 | private boolean stillRunning() { 125 | return applicationThread != null && applicationThread.isAlive(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /python/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | ../ruby/lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | /.idea -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | Python 2 | ===== 3 | Usage 4 | ----- 5 | The application requires Python 3.5 or above 6 | 7 | First, create a virtual environment 8 | ``` 9 | pip install virtualenv 10 | virtualenv venv 11 | ``` 12 | 13 | To activate venv on Windows 14 | ``` 15 | venv\Scripts\activate 16 | ``` 17 | 18 | To activate venv on Linux 19 | ``` 20 | source venv/bin/activate 21 | ``` 22 | 23 | To run tests: 24 | ``` 25 | python -m unittest -v 26 | ``` 27 | 28 | To run the application: 29 | ``` 30 | python -m task_list 31 | ``` 32 | 33 | Notes on testing 34 | ---------------- 35 | For end-to-end testing, a subprocess was used instead of threading. The subprocess module allows 36 | you to create a new process and connect to their input and output pipes. 37 | 38 | The application test runs main from task_list module and then injects the inputs into stdin. 39 | The IO Pipe is blocking which more closely emulates the real behavior of stdin when calling readline. 40 | The call will block until data is written to stdin. 41 | Likewise stdout.read will block until there is data to be read. 42 | 43 | Because of the potential for blocking, a threaded timer was introduced 44 | to kill the subprocess on deadlock. The timeout is currently set to 2 seconds 45 | but if additional tests run longer than that the timeout should be increased. 46 | 47 | As the subprocess captures all input and output, using print statements for debugging during tests 48 | will not work as expected. -------------------------------------------------------------------------------- /python/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='task-list', 5 | version='0.0.1', 6 | packages=['task_list'], 7 | url='https://github.com/codurance/task-list', 8 | description='Task List Kata', 9 | entry_points={ 10 | 'console_scripts': [ 11 | "task_list = task_list.__main__:main", 12 | ] 13 | } 14 | ) 15 | -------------------------------------------------------------------------------- /python/task_list/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codurance/task-list/d9218b2cb31894c53fe221a6cf8dfc311a6919ee/python/task_list/__init__.py -------------------------------------------------------------------------------- /python/task_list/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from task_list.console import Console 4 | from task_list.app import TaskList 5 | 6 | 7 | def main(): 8 | task_list = TaskList(Console(sys.stdin, sys.stdout)) 9 | task_list.run() 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | 15 | -------------------------------------------------------------------------------- /python/task_list/app.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from task_list.console import Console 4 | from task_list.task import Task 5 | 6 | 7 | class TaskList: 8 | QUIT = "quit" 9 | 10 | def __init__(self, console: Console) -> None: 11 | self.console = console 12 | self.last_id: int = 0 13 | self.tasks: Dict[str, List[Task]] = dict() 14 | 15 | def run(self) -> None: 16 | while True: 17 | command = self.console.input("> ") 18 | if command == self.QUIT: 19 | break 20 | self.execute(command) 21 | 22 | def execute(self, command_line: str) -> None: 23 | command_rest = command_line.split(" ", 1) 24 | command = command_rest[0] 25 | if command == "show": 26 | self.show() 27 | elif command == "add": 28 | self.add(command_rest[1]) 29 | elif command == "check": 30 | self.check(command_rest[1]) 31 | elif command == "uncheck": 32 | self.uncheck(command_rest[1]) 33 | elif command == "help": 34 | self.help() 35 | else: 36 | self.error(command) 37 | 38 | def show(self) -> None: 39 | for project, tasks in self.tasks.items(): 40 | self.console.print(project) 41 | for task in tasks: 42 | self.console.print(f" [{'x' if task.is_done() else ' '}] {task.id}: {task.description}") 43 | self.console.print() 44 | 45 | def add(self, command_line: str) -> None: 46 | sub_command_rest = command_line.split(" ", 1) 47 | sub_command = sub_command_rest[0] 48 | if sub_command == "project": 49 | self.add_project(sub_command_rest[1]) 50 | elif sub_command == "task": 51 | project_task = sub_command_rest[1].split(" ", 1) 52 | self.add_task(project_task[0], project_task[1]) 53 | 54 | def add_project(self, name: str) -> None: 55 | self.tasks[name] = [] 56 | 57 | def add_task(self, project: str, description: str) -> None: 58 | project_tasks = self.tasks.get(project) 59 | if project_tasks is None: 60 | self.console.print(f"Could not find a project with the name {project}.") 61 | self.console.print() 62 | return 63 | project_tasks.append(Task(self.next_id(), description, False)) 64 | 65 | def check(self, id_string: str) -> None: 66 | self.set_done(id_string, True) 67 | 68 | def uncheck(self, id_string: str) -> None: 69 | self.set_done(id_string, False) 70 | 71 | def set_done(self, id_string: str, done: bool) -> None: 72 | id_ = int(id_string) 73 | for project, tasks in self.tasks.items(): 74 | for task in tasks: 75 | if task.id == id_: 76 | task.set_done(done) 77 | return 78 | self.console.print(f"Could not find a task with an ID of {id_}") 79 | self.console.print() 80 | 81 | def help(self) -> None: 82 | self.console.print("Commands:") 83 | self.console.print(" show") 84 | self.console.print(" add project ") 85 | self.console.print(" add task ") 86 | self.console.print(" check ") 87 | self.console.print(" uncheck ") 88 | self.console.print() 89 | 90 | def error(self, command: str) -> None: 91 | self.console.print(f"I don't know what the command {command} is.") 92 | self.console.print() 93 | 94 | def next_id(self) -> int: 95 | self.last_id += 1 96 | return self.last_id 97 | 98 | -------------------------------------------------------------------------------- /python/task_list/console.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, IO 2 | 3 | 4 | class Console: 5 | def __init__(self, input_reader: IO, output_writer: IO) -> None: 6 | self.input_reader = input_reader 7 | self.output_writer = output_writer 8 | 9 | def print(self, string: Optional[str]="", end: str="\n", flush: bool=True) -> None: 10 | self.output_writer.write(string + end) 11 | if flush: 12 | self.output_writer.flush() 13 | 14 | def input(self, prompt: Optional[str]="") -> str: 15 | self.print(prompt, end="") 16 | return self.input_reader.readline().strip() 17 | 18 | 19 | -------------------------------------------------------------------------------- /python/task_list/task.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Task: 4 | def __init__(self, id_: int, description: str, done: bool) -> None: 5 | self.id = id_ 6 | self.description = description 7 | self.done = done 8 | 9 | def set_done(self, done: bool) -> None: 10 | self.done = done 11 | 12 | def is_done(self) -> bool: 13 | return self.done 14 | 15 | -------------------------------------------------------------------------------- /python/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codurance/task-list/d9218b2cb31894c53fe221a6cf8dfc311a6919ee/python/tests/__init__.py -------------------------------------------------------------------------------- /python/tests/test_application.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import unittest 3 | from threading import Timer 4 | 5 | 6 | class ApplicationTest(unittest.TestCase): 7 | PROMPT = "> " 8 | TIMEOUT = 2 9 | 10 | def setUp(self): 11 | self.proc = subprocess.Popen( 12 | ["python", "-m", "task_list"], 13 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, 14 | universal_newlines=True) 15 | self.timer = Timer(self.TIMEOUT, self.proc.kill) 16 | self.timer.start() 17 | 18 | def tearDown(self): 19 | self.timer.cancel() 20 | self.proc.stdout.close() 21 | self.proc.stdin.close() 22 | while self.proc.returncode is None: 23 | self.proc.poll() 24 | 25 | def test_it_works(self): 26 | self.execute("show") 27 | self.execute("add project secrets") 28 | self.execute("add task secrets Eat more donuts.") 29 | self.execute("add task secrets Destroy all humans.") 30 | self.execute("show") 31 | 32 | self.read_lines( 33 | "secrets", 34 | " [ ] 1: Eat more donuts.", 35 | " [ ] 2: Destroy all humans.", 36 | "") 37 | 38 | self.execute("add project training") 39 | self.execute("add task training Four Elements of Simple Design") 40 | self.execute("add task training SOLID") 41 | self.execute("add task training Coupling and Cohesion") 42 | self.execute("add task training Primitive Obsession") 43 | self.execute("add task training Outside-In TDD") 44 | self.execute("add task training Interaction-Driven Design") 45 | 46 | self.execute("check 1") 47 | self.execute("check 3") 48 | self.execute("check 5") 49 | self.execute("check 6") 50 | self.execute("show") 51 | 52 | self.read_lines( 53 | "secrets", 54 | " [x] 1: Eat more donuts.", 55 | " [ ] 2: Destroy all humans.", 56 | "", 57 | "training", 58 | " [x] 3: Four Elements of Simple Design", 59 | " [ ] 4: SOLID", 60 | " [x] 5: Coupling and Cohesion", 61 | " [x] 6: Primitive Obsession", 62 | " [ ] 7: Outside-In TDD", 63 | " [ ] 8: Interaction-Driven Design", 64 | "") 65 | 66 | self.execute("quit") 67 | 68 | def execute(self, command): 69 | self.write(command + "\n") 70 | 71 | def write(self, command): 72 | self.read(self.PROMPT) 73 | self.proc.stdin.write(command) 74 | self.proc.stdin.flush() 75 | 76 | def read(self, expected_output): 77 | output = self.proc.stdout.read(len(expected_output)) 78 | self.assertEqual(expected_output, output) 79 | 80 | def read_lines(self, *lines): 81 | for line in lines: 82 | self.read(line + "\n") 83 | -------------------------------------------------------------------------------- /ruby/.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | -------------------------------------------------------------------------------- /ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :test do 4 | gem 'rspec' 5 | end 6 | -------------------------------------------------------------------------------- /ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | diff-lcs (1.2.5) 5 | rspec (3.5.0) 6 | rspec-core (~> 3.5.0) 7 | rspec-expectations (~> 3.5.0) 8 | rspec-mocks (~> 3.5.0) 9 | rspec-core (3.5.1) 10 | rspec-support (~> 3.5.0) 11 | rspec-expectations (3.5.0) 12 | diff-lcs (>= 1.2.0, < 2.0) 13 | rspec-support (~> 3.5.0) 14 | rspec-mocks (3.5.0) 15 | diff-lcs (>= 1.2.0, < 2.0) 16 | rspec-support (~> 3.5.0) 17 | rspec-support (3.5.0) 18 | 19 | PLATFORMS 20 | ruby 21 | 22 | DEPENDENCIES 23 | rspec 24 | 25 | BUNDLED WITH 26 | 1.12.5 27 | -------------------------------------------------------------------------------- /ruby/lib/task.rb: -------------------------------------------------------------------------------- 1 | class Task 2 | attr_reader :id, :description 3 | attr_accessor :done 4 | 5 | def initialize(id, description, done) 6 | @id = id 7 | @description = description 8 | @done = done 9 | end 10 | 11 | def done? 12 | done 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /ruby/lib/task_list.rb: -------------------------------------------------------------------------------- 1 | require_relative 'task' 2 | 3 | class TaskList 4 | QUIT = 'quit' 5 | 6 | def initialize(input, output) 7 | @input = input 8 | @output = output 9 | 10 | @tasks = {} 11 | end 12 | 13 | def run 14 | while true 15 | @output.print('> ') 16 | @output.flush 17 | 18 | command = @input.readline.strip 19 | break if command == QUIT 20 | 21 | execute(command) 22 | end 23 | end 24 | 25 | private 26 | 27 | def execute(command_line) 28 | command, rest = command_line.split(/ /, 2) 29 | case command 30 | when 'show' 31 | show 32 | when 'add' 33 | add rest 34 | when 'check' 35 | check rest 36 | when 'uncheck' 37 | uncheck rest 38 | when 'help' 39 | help 40 | else 41 | error command 42 | end 43 | end 44 | 45 | def show 46 | @tasks.each do |project_name, project_tasks| 47 | @output.puts project_name 48 | project_tasks.each do |task| 49 | @output.printf(" [%c] %d: %s\n", (task.done? ? 'x' : ' '), task.id, task.description) 50 | end 51 | @output.puts 52 | end 53 | end 54 | 55 | def add(command_line) 56 | subcommand, rest = command_line.split(/ /, 2) 57 | if subcommand == 'project' 58 | add_project rest 59 | elsif subcommand == 'task' 60 | project, description = rest.split(/ /, 2) 61 | add_task project, description 62 | end 63 | end 64 | 65 | def add_project(name) 66 | @tasks[name] = [] 67 | end 68 | 69 | def add_task(project, description) 70 | project_tasks = @tasks[project] 71 | if project_tasks.nil? 72 | @output.printf("Could not find a project with the name \"%s\".\n", project) 73 | return 74 | end 75 | project_tasks << Task.new(next_id, description, false) 76 | end 77 | 78 | def check(id_string) 79 | set_done(id_string, true) 80 | end 81 | 82 | def uncheck(id_string) 83 | set_done(id_string, false) 84 | end 85 | 86 | def set_done(id_string, done) 87 | id = id_string.to_i 88 | task = @tasks.collect { |project_name, project_tasks| 89 | project_tasks.find { |t| t.id == id } 90 | }.reject(&:nil?).first 91 | 92 | if task.nil? 93 | @output.printf("Could not find a task with an ID of %d.\n", id) 94 | return 95 | end 96 | 97 | task.done = done 98 | end 99 | 100 | def help 101 | @output.puts('Commands:') 102 | @output.puts(' show') 103 | @output.puts(' add project ') 104 | @output.puts(' add task ') 105 | @output.puts(' check ') 106 | @output.puts(' uncheck ') 107 | @output.puts() 108 | end 109 | 110 | def error(command) 111 | @output.printf("I don't know what the command \"%s\" is.\n", command) 112 | end 113 | 114 | def next_id 115 | @last_id ||= 0 116 | @last_id += 1 117 | @last_id 118 | end 119 | end 120 | 121 | if __FILE__ == $0 122 | TaskList.new($stdin, $stdout).run 123 | end 124 | -------------------------------------------------------------------------------- /ruby/spec/application_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'stringio' 3 | require 'timeout' 4 | 5 | require_relative '../lib/task_list' 6 | 7 | describe 'application' do 8 | PROMPT = '> ' 9 | 10 | around :each do |example| 11 | @input_reader, @input_writer = IO.pipe 12 | @output_reader, @output_writer = IO.pipe 13 | 14 | application = TaskList.new(@input_reader, @output_writer) 15 | @application_thread = Thread.new do 16 | application.run 17 | end 18 | @application_thread.abort_on_exception = true 19 | 20 | example.run 21 | 22 | @input_reader.close 23 | @input_writer.close 24 | @output_reader.close 25 | @output_writer.close 26 | end 27 | 28 | after :each do 29 | next unless still_running? 30 | sleep 1 31 | next unless still_running? 32 | @application_thread.kill 33 | raise 'The application is still running.' 34 | end 35 | 36 | it 'works' do 37 | Timeout::timeout 1 do 38 | execute('show') 39 | 40 | execute('add project secrets') 41 | execute('add task secrets Eat more donuts.') 42 | execute('add task secrets Destroy all humans.') 43 | 44 | execute('show') 45 | read_lines( 46 | 'secrets', 47 | ' [ ] 1: Eat more donuts.', 48 | ' [ ] 2: Destroy all humans.', 49 | '' 50 | ) 51 | 52 | execute('add project training') 53 | execute('add task training Four Elements of Simple Design') 54 | execute('add task training SOLID') 55 | execute('add task training Coupling and Cohesion') 56 | execute('add task training Primitive Obsession') 57 | execute('add task training Outside-In TDD') 58 | execute('add task training Interaction-Driven Design') 59 | 60 | execute('check 1') 61 | execute('check 3') 62 | execute('check 5') 63 | execute('check 6') 64 | 65 | execute('show') 66 | read_lines( 67 | 'secrets', 68 | ' [x] 1: Eat more donuts.', 69 | ' [ ] 2: Destroy all humans.', 70 | '', 71 | 'training', 72 | ' [x] 3: Four Elements of Simple Design', 73 | ' [ ] 4: SOLID', 74 | ' [x] 5: Coupling and Cohesion', 75 | ' [x] 6: Primitive Obsession', 76 | ' [ ] 7: Outside-In TDD', 77 | ' [ ] 8: Interaction-Driven Design', 78 | '' 79 | ) 80 | 81 | execute('quit') 82 | end 83 | end 84 | 85 | def execute(command) 86 | read PROMPT 87 | write command 88 | end 89 | 90 | def read(expected_output) 91 | actual_output = @output_reader.read(expected_output.length) 92 | expect(actual_output).to eq expected_output 93 | end 94 | 95 | def read_lines(*expected_output) 96 | expected_output.each do |line| 97 | read "#{line}\n" 98 | end 99 | end 100 | 101 | def write(input) 102 | @input_writer.puts input 103 | end 104 | 105 | def still_running? 106 | @application_thread && @application_thread.alive? 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /scala/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | target/ 4 | -------------------------------------------------------------------------------- /scala/build.sbt: -------------------------------------------------------------------------------- 1 | lazy val settings = Seq( 2 | name := "tasks", 3 | organization := "com.codurance.training", 4 | version := "0.1-SNAPSHOT", 5 | scalaVersion := "2.11.8" 6 | ) 7 | 8 | lazy val root = (project in file(".")).settings(settings: _*) 9 | 10 | libraryDependencies += "org.scalatest" %% "scalatest" % "2.2.6" % Test 11 | -------------------------------------------------------------------------------- /scala/src/main/scala/TaskList.scala: -------------------------------------------------------------------------------- 1 | import scala.collection.mutable 2 | import scala.collection.mutable.ArrayBuffer 3 | import scala.io.StdIn 4 | 5 | case class Task(id: Long, description: String, var done: Boolean = false) 6 | 7 | class TaskList extends Runnable { 8 | 9 | private val tasks = mutable.HashMap[String, Seq[Task]]() 10 | 11 | override def run(): Unit = { 12 | var finished = false 13 | while(!finished) { 14 | StdIn.readLine("» ") match { 15 | case "quit" => finished = true 16 | case commandLine => execute(commandLine) 17 | } 18 | } 19 | } 20 | 21 | private def execute(commandLine: String): Unit = { 22 | val commandParts = commandLine.split("""\s""", 2) 23 | val command = commandParts(0) 24 | command match { 25 | case "" => () 26 | case "help" => help() 27 | case "show" => show() 28 | case "add" => add(commandParts(1)) 29 | case "check" => check(commandParts(1)) 30 | case "uncheck" => uncheck(commandParts(1)) 31 | case _ => showError(command) 32 | } 33 | } 34 | 35 | private def help(): Unit = { 36 | Console.println( 37 | """Commands: 38 | | show 39 | | add project 40 | | add task 41 | | check 42 | | uncheck 43 | """.stripMargin) 44 | } 45 | 46 | private def show(): Unit = { 47 | tasks foreach { case (project, projectTasks) => 48 | Console.println(project) 49 | projectTasks foreach { task => 50 | val checkbox = if (task.done) "[x]" else "[ ]" 51 | Console.println(s" $checkbox ${task.id}: ${task.description}") 52 | } 53 | Console.println() 54 | } 55 | } 56 | 57 | private def add(commandLine: String): Unit = { 58 | val commandWords = commandLine.split("""\s""", 2) 59 | val command = commandWords(0) 60 | command match { 61 | case "project" => addProject(commandWords(1)) 62 | case "task" => { 63 | val projectTask = commandWords(1).split("""\s""", 2) 64 | addTask(projectTask(0), projectTask(1)) 65 | } 66 | case _ => showError(commandLine) 67 | } 68 | } 69 | 70 | private def addProject(name: String): Unit = { 71 | tasks += name -> Seq[Task]() 72 | } 73 | 74 | private def addTask(project: String, description: String): Unit = { 75 | tasks.get(project).map { projectTasks => 76 | tasks.put(project, projectTasks :+ Task(nextId, description)) 77 | }.getOrElse { 78 | Console.println(Console.RED + "Could not find a project with the name " + project + Console.WHITE) 79 | } 80 | } 81 | 82 | private def nextId: Long = { 83 | (0L +: tasks.values.flatten.map(_.id).toList).max + 1L 84 | } 85 | 86 | private def check(idString: String): Unit = setDone(idString, true) 87 | 88 | private def uncheck(idString: String): Unit = setDone(idString, false) 89 | 90 | private def setDone(idString: String, done: Boolean): Unit = { 91 | tasks.values.flatten.find(_.id == idString.toLong).foreach { task => 92 | task.done = done 93 | } 94 | } 95 | 96 | private def showError(command: String): Unit = { 97 | Console.println(Console.RED + "Unknown command: " + command + Console.WHITE) 98 | } 99 | } 100 | 101 | object TaskList { 102 | def main( args:Array[String] ): Unit = { 103 | (new TaskList).run() 104 | } 105 | } -------------------------------------------------------------------------------- /scala/src/test/scala/TaskListSpec.scala: -------------------------------------------------------------------------------- 1 | import java.io._ 2 | 3 | import org.scalatest.{BeforeAndAfter, FlatSpec} 4 | 5 | class TaskListSpec extends FlatSpec with BeforeAndAfter { 6 | 7 | var applicationThread: Thread = null 8 | 9 | val inStream = new PipedOutputStream() 10 | val inWriter = new PrintWriter(inStream, true) 11 | 12 | val outStream = new PipedInputStream() 13 | val outReader = new BufferedReader(new InputStreamReader(outStream)) 14 | 15 | before { 16 | val in = new BufferedReader(new InputStreamReader(new PipedInputStream(inStream))) 17 | val out = new PipedOutputStream(outStream) 18 | scala.Console.withOut(out) { 19 | scala.Console.withIn(in) { 20 | applicationThread = new Thread(new TaskList) 21 | applicationThread.start() 22 | } 23 | } 24 | } 25 | 26 | after { 27 | if (applicationThread != null && applicationThread.isAlive) { 28 | applicationThread.interrupt() 29 | throw new IllegalStateException("The application is still running.") 30 | } 31 | } 32 | 33 | "A task list" should "work" in { 34 | 35 | executeCommand("show") 36 | 37 | executeCommand("add project secrets") 38 | executeCommand("add task secrets Eat more donuts.") 39 | executeCommand("add task secrets Destroy all humans.") 40 | 41 | executeCommand("show") 42 | readLines( 43 | "secrets", 44 | " [ ] 1: Eat more donuts.", 45 | " [ ] 2: Destroy all humans.", 46 | "") 47 | 48 | executeCommand("add project training") 49 | executeCommand("add task training Four Elements of Simple Design") 50 | executeCommand("add task training SOLID") 51 | executeCommand("add task training Coupling and Cohesion") 52 | executeCommand("add task training Primitive Obsession") 53 | executeCommand("add task training Outside-In TDD") 54 | executeCommand("add task training Interaction-Driven Design") 55 | 56 | executeCommand("check 1") 57 | executeCommand("check 3") 58 | executeCommand("check 5") 59 | executeCommand("check 6") 60 | 61 | executeCommand("show") 62 | readLines( 63 | "secrets", 64 | " [x] 1: Eat more donuts.", 65 | " [ ] 2: Destroy all humans.", 66 | "", 67 | "training", 68 | " [x] 3: Four Elements of Simple Design", 69 | " [ ] 4: SOLID", 70 | " [x] 5: Coupling and Cohesion", 71 | " [x] 6: Primitive Obsession", 72 | " [ ] 7: Outside-In TDD", 73 | " [ ] 8: Interaction-Driven Design", 74 | "") 75 | 76 | executeCommand("quit") 77 | } 78 | 79 | def executeCommand(command: String): Unit = { 80 | read("» ") 81 | inWriter.println(command) 82 | } 83 | 84 | def readLines(expectedOutput: String*): Unit = { 85 | expectedOutput.foreach { line => 86 | read(line + System.lineSeparator()) 87 | } 88 | } 89 | 90 | def read(expectedOutput: String): Unit = { 91 | val length = expectedOutput.length() 92 | val buffer = new Array[Char](length) 93 | outReader.read(buffer, 0, length) 94 | assert(String.valueOf(buffer) == expectedOutput) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /typescript/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | typings/ 3 | src/*.js 4 | src/*.js.map 5 | tests/*.js 6 | tests/*.js.map 7 | -------------------------------------------------------------------------------- /typescript/README: -------------------------------------------------------------------------------- 1 | Setup 2 | ===== 3 | 4 | 1. Install npm: https://www.npmjs.com/ 5 | 6 | 2. Install the dependencies: 7 | > npm install 8 | 9 | Usage 10 | ===== 11 | 12 | Run the tests: 13 | > npm test 14 | 15 | Run the application: 16 | > npm start 17 | 18 | TODO 19 | ==== 20 | 21 | The tests are bit funny at the moment because nodeunit assertions only 22 | fail at the end of the test. Fortunately the built in assertions format nicely 23 | and won't prevent additional tests from running so I use them mostly. 24 | Except one at the end to keep the grunt nodeunit plugin happy. 25 | This could be improved by migrating to a different test framework. 26 | Mocha looks good. 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-list", 3 | "version": "0.0.0", 4 | "description": "task-list primitives exercise ported to typescript", 5 | "main": "task_list.js", 6 | "scripts": { 7 | "install": "tsd reinstall", 8 | "build": "tsc -t ES5 -m commonjs src/*.ts tests/*.ts", 9 | "start": "npm run-script build; node src/task_list.js", 10 | "test": "npm run-script build; nodeunit tests" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "readline": "0.0.3" 16 | }, 17 | "devDependencies": { 18 | "nodeunit": "^0.9.0", 19 | "tsd": "^0.5.7", 20 | "typescript": "^1.3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /typescript/src/task.ts: -------------------------------------------------------------------------------- 1 | 2 | export class Task 3 | { 4 | constructor(private _id: number, private _description: string, private _done: boolean) {} 5 | 6 | get id() { 7 | return this._id; 8 | } 9 | 10 | get description() { 11 | return this._description; 12 | } 13 | 14 | get done() { 15 | return this._done; 16 | } 17 | 18 | set done(val: boolean) { 19 | this._done = val; 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /typescript/src/task_list.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import readline = require('readline'); 4 | import util = require('util'); 5 | 6 | import task = require('./task'); 7 | 8 | function splitFirstSpace(s: string) { 9 | var pos = s.indexOf(' '); 10 | if(pos === -1) { 11 | return [s]; 12 | } 13 | return [s.substr(0, pos), s.substr(pos+1)] 14 | } 15 | 16 | export class TaskList 17 | { 18 | static QUIT = 'quit'; 19 | private readline; 20 | private tasks: {[index: string]: task.Task[]} = {}; 21 | private lastId = 0; 22 | 23 | constructor(reader: NodeJS.ReadableStream, writer: NodeJS.WritableStream) { 24 | 25 | this.readline = readline.createInterface({ 26 | terminal: false, 27 | input: reader, 28 | output: writer 29 | }); 30 | 31 | this.readline.setPrompt("> "); 32 | this.readline.on('line', (cmd) => { 33 | if(cmd == TaskList.QUIT) { 34 | this.readline.close(); 35 | return; 36 | } 37 | this.execute(cmd); 38 | this.readline.prompt(); 39 | }); 40 | this.readline.on('close', () => { 41 | writer.end(); 42 | }); 43 | } 44 | 45 | println(ln: string) { 46 | this.readline.output.write(ln); 47 | this.readline.output.write('\n'); 48 | } 49 | 50 | run() { 51 | this.readline.prompt(); 52 | } 53 | 54 | forEachProject(func: (key: string, value: task.Task[]) => any) { 55 | for(var key in this.tasks) { 56 | if(this.tasks.hasOwnProperty(key)) 57 | func(key, this.tasks[key]) 58 | } 59 | } 60 | 61 | execute(commandLine: string) { 62 | var commandRest = splitFirstSpace(commandLine); 63 | var command = commandRest[0]; 64 | switch (command) { 65 | case "show": 66 | this.show(); 67 | break; 68 | case "add": 69 | this.add(commandRest[1]); 70 | break; 71 | case "check": 72 | this.check(commandRest[1]); 73 | break; 74 | case "uncheck": 75 | this.uncheck(commandRest[1]); 76 | break; 77 | case "help": 78 | this.help(); 79 | break; 80 | default: 81 | this.error(command); 82 | break; 83 | } 84 | } 85 | 86 | private show() { 87 | this.forEachProject((project, taskList) => { 88 | this.println(project); 89 | taskList.forEach((task) => { 90 | this.println(util.format(" [%s] %d: %s", (task.done ? 'x' : ' '), task.id, task.description)); 91 | }); 92 | this.println(''); 93 | }); 94 | } 95 | 96 | private add(commandLine: string) { 97 | var subcommandRest = splitFirstSpace(commandLine); 98 | var subcommand = subcommandRest[0]; 99 | if (subcommand === "project") { 100 | this.addProject(subcommandRest[1]); 101 | } else if (subcommand === "task") { 102 | var projectTask = splitFirstSpace(subcommandRest[1]); 103 | this.addTask(projectTask[0], projectTask[1]); 104 | } 105 | } 106 | 107 | private addProject(name: string) { 108 | this.tasks[name] = []; 109 | } 110 | 111 | private addTask(project: string, description: string) { 112 | var projectTasks = this.tasks[project]; 113 | if (projectTasks == null) { 114 | this.println(util.format("Could not find a project with the name \"%s\".", project)); 115 | return; 116 | } 117 | projectTasks.push(new task.Task(this.nextId(), description, false)); 118 | } 119 | 120 | private check(idString: string) { 121 | this.setDone(idString, true); 122 | } 123 | 124 | private uncheck(idString: string) { 125 | this.setDone(idString, false); 126 | } 127 | 128 | private setDone(idString: string, done: boolean) { 129 | var id = parseInt(idString, 10); 130 | var found = false; 131 | this.forEachProject((project, taskList) => { 132 | taskList.forEach((task) => { 133 | if (task.id == id) { 134 | task.done = done; 135 | found = true; 136 | } 137 | }); 138 | }); 139 | if(!found) 140 | this.println(util.format("Could not find a task with an ID of %d.", id)); 141 | } 142 | 143 | private help() { 144 | this.println("Commands:"); 145 | this.println(" show"); 146 | this.println(" add project "); 147 | this.println(" add task "); 148 | this.println(" check "); 149 | this.println(" uncheck "); 150 | this.println(""); 151 | } 152 | 153 | private error(command: string) { 154 | this.println('I don\'t know what the command "' + command + '" is.'); 155 | } 156 | 157 | private nextId(): number { 158 | return ++this.lastId; 159 | } 160 | } 161 | 162 | if(require.main == module) { 163 | new TaskList(process.stdin, process.stdout).run() 164 | } 165 | -------------------------------------------------------------------------------- /typescript/tests/application_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import assert = require('assert'); 4 | import nodeunit = require('nodeunit'); 5 | import stream = require('stream'); 6 | import task_list = require('../src/task_list'); 7 | 8 | class Expectation { 9 | constructor(public ctxt: TestCtxt) { 10 | this.ctxt.expectations.push(this) 11 | } 12 | 13 | test(): boolean { 14 | this.ctxt.test.ok(false, "Override me"); 15 | return true; 16 | } 17 | } 18 | 19 | class TestCtxt 20 | { 21 | input = new stream.PassThrough(); 22 | output = new stream.PassThrough(); 23 | expectations : Expectation[] = []; 24 | tl = new task_list.TaskList(this.input, this.output); 25 | 26 | constructor(public test: nodeunit.Test){ 27 | var count = 0; 28 | this.output.on('readable', () => { 29 | if(count > this.expectations.length) { 30 | this.test.ok(false, "Got more output than expected proabably didn't quit") 31 | this.test.done(); 32 | } else if(count == this.expectations.length) { 33 | this.test.ok(true); 34 | this.test.done(); 35 | } else if(this.expectations[count].test()) { 36 | count += 1; 37 | } 38 | }); 39 | this.output.on('end', () => { 40 | this.test.equal(count, this.expectations.length); 41 | this.test.done(); 42 | }); 43 | } 44 | 45 | read(expected) { 46 | var data = this.output.read(expected.length); 47 | if (data != null) { 48 | data = data.toString(); 49 | assert.equal(data, expected); 50 | return true; 51 | } 52 | return false; 53 | } 54 | 55 | run() { 56 | this.test.expect(1); 57 | this.tl.run(); 58 | } 59 | } 60 | 61 | class ExecuteExpectation extends Expectation { 62 | prompt = '> '; 63 | 64 | constructor(ctxt: TestCtxt, public cmd:string) { 65 | super(ctxt) 66 | } 67 | 68 | test() { 69 | if (!this.ctxt.read(this.prompt)) 70 | return false; 71 | this.ctxt.input.write(this.cmd + '\n'); 72 | return true; 73 | } 74 | } 75 | 76 | class OutputExpectation extends Expectation { 77 | 78 | constructor(ctxt: TestCtxt, public out:string) { 79 | super(ctxt) 80 | } 81 | 82 | test() { 83 | return this.ctxt.read(this.out) 84 | } 85 | } 86 | 87 | export function application_test(test: nodeunit.Test) { 88 | var ctxt = new TestCtxt(test); 89 | 90 | function execute(cmd: string) { 91 | new ExecuteExpectation(ctxt, cmd); 92 | } 93 | 94 | function readLines(...strings: string[]) { 95 | strings.forEach((s) => { 96 | new OutputExpectation(ctxt, s + '\n'); 97 | }) 98 | } 99 | 100 | execute('show'); 101 | 102 | execute("add project secrets"); 103 | execute("add task secrets Eat more donuts."); 104 | execute("add task secrets Destroy all humans."); 105 | 106 | execute("show"); 107 | readLines( 108 | "secrets", 109 | " [ ] 1: Eat more donuts.", 110 | " [ ] 2: Destroy all humans.", 111 | "" 112 | ); 113 | 114 | execute("add project training"); 115 | execute("add task training Four Elements of Simple Design"); 116 | execute("add task training SOLID"); 117 | execute("add task training Coupling and Cohesion"); 118 | execute("add task training Primitive Obsession"); 119 | execute("add task training Outside-In TDD"); 120 | execute("add task training Interaction-Driven Design"); 121 | 122 | execute("check 1"); 123 | execute("check 3"); 124 | execute("check 5"); 125 | execute("check 6"); 126 | 127 | execute("show"); 128 | readLines( 129 | "secrets", 130 | " [x] 1: Eat more donuts.", 131 | " [ ] 2: Destroy all humans.", 132 | "", 133 | "training", 134 | " [x] 3: Four Elements of Simple Design", 135 | " [ ] 4: SOLID", 136 | " [x] 5: Coupling and Cohesion", 137 | " [x] 6: Primitive Obsession", 138 | " [ ] 7: Outside-In TDD", 139 | " [ ] 8: Interaction-Driven Design", 140 | "" 141 | ); 142 | execute('quit'); 143 | 144 | ctxt.run(); 145 | } 146 | -------------------------------------------------------------------------------- /typescript/tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "node/node.d.ts": { 9 | "commit": "1c3488c2d84232b9f66b6d7e758839381e95b3bf" 10 | }, 11 | "nodeunit/nodeunit.d.ts": { 12 | "commit": "1c3488c2d84232b9f66b6d7e758839381e95b3bf" 13 | } 14 | } 15 | } 16 | --------------------------------------------------------------------------------