├── .dockerignore ├── go.mod ├── Dockerfile ├── README.md ├── .gitattributes ├── main.go ├── .gitignore └── main_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module golang-temperature-converter-cli 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 2 | 3 | ENV CGO_ENABLED 0 4 | 5 | WORKDIR /src/app 6 | 7 | RUN addgroup --system projects && adduser --system projects --ingroup projects 8 | 9 | RUN chown -R projects:projects /src/app 10 | 11 | USER projects 12 | 13 | COPY . . 14 | 15 | RUN go install -v ./... -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Temperature Converter CLI 2 | 3 | Converts temperature between Fahrenheit and Celsius. 4 | 5 | ## Usage 6 | 7 | Compile with `go build -o temp`. 8 | 9 | Then, invoke the binary passing as argument the unit of temperature we want to convert **from**. 10 | For example: 11 | 12 | `./temp C` to convert from Celsius to Fahrenheit or `./temp F` to convert from Fahrenheit to Celsius. 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://help.github.com/en/articles/configuring-git-to-handle-line-endings 2 | # Set the default behavior, in case people don't have core.autocrlf set. 3 | * text=auto 4 | 5 | # Explicitly declare text files you want to always be normalized and converted 6 | # to native line endings on checkout. 7 | *.c text 8 | *.h text 9 | 10 | # Declare files that will always have CRLF line endings on checkout. 11 | *.sln text eol=crlf 12 | 13 | # Denote all files that are truly binary and should not be modified. 14 | *.png binary 15 | *.jpg binary 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | var originUnit string 10 | var originValue float64 11 | 12 | var shouldConvertAgain string 13 | 14 | var err error 15 | 16 | var errInvalidArguments = errors.New("Invalid arguments") 17 | var errReadingInput = errors.New("Error reading input") 18 | 19 | func main() { 20 | 21 | for { 22 | fmt.Print("What is the current temperature in " + originUnit + " ? ") 23 | 24 | fmt.Print("Would you like to convert another temperature ? (y/n) ") 25 | 26 | if shouldConvertAgain != "Y" { 27 | fmt.Println("Good bye!") 28 | break 29 | } 30 | } 31 | } 32 | 33 | func printError(err error) { 34 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 35 | os.Exit(1) 36 | } 37 | 38 | func convertToCelsius(value float64) { 39 | convertedValue := (value - 32) * 5 / 9 40 | fmt.Printf("%v F = %.0f C\n", value, convertedValue) 41 | } 42 | 43 | func convertToFahrenheit(value float64) { 44 | convertedValue := (value * 9 / 5) + 32 45 | fmt.Printf("%v C = %.0f F\n", value, convertedValue) 46 | } 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "path" 13 | "reflect" 14 | "strings" 15 | "testing" 16 | ) 17 | 18 | var binaryName = "tempconverterTestBinary" 19 | 20 | func TestMain(m *testing.M) { 21 | build := exec.Command("go", "build", "-o", binaryName) 22 | err := build.Run() 23 | if err != nil { 24 | fmt.Printf("could not make binary %v", err) 25 | os.Exit(1) 26 | } 27 | exitCode := m.Run() 28 | 29 | cleanUp := exec.Command("rm", "-f", binaryName) 30 | cleanUperr := cleanUp.Run() 31 | if cleanUperr != nil { 32 | fmt.Println("could not clean up", err) 33 | } 34 | 35 | os.Exit(exitCode) 36 | } 37 | 38 | func TestCheckForArgumentsM1(t *testing.T) { 39 | t.Run("invalid args", func(t *testing.T) { 40 | dir, err := os.Getwd() 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | // Runs the program with not enough arguments. 45 | cmd := exec.Command(path.Join(dir, binaryName), []string{}...) 46 | output, err := cmd.CombinedOutput() 47 | if err == nil || !strings.Contains(string(output), errInvalidArguments.Error()) { 48 | t.Fatal("Did not validate command line arguments properly") 49 | } 50 | 51 | // Runs the program with more than enough 52 | cmd2 := exec.Command(path.Join(dir, binaryName), []string{"one", "two"}...) 53 | output2, err2 := cmd2.CombinedOutput() 54 | if err2 == nil || !strings.Contains(string(output2), errInvalidArguments.Error()) { 55 | t.Fatal("Did not validate command line arguments properly") 56 | } 57 | }) 58 | } 59 | 60 | func TestAssignsToOriginUnitM1(t *testing.T) { 61 | isAssigningToOriginUnit := false 62 | isAssigningToOriginUnitFromCorrectFunction := false 63 | 64 | src, err := ioutil.ReadFile("main.go") 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | 69 | fset := token.NewFileSet() 70 | f, err := parser.ParseFile(fset, "", src, 0) 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | for _, decl := range f.Decls { 76 | if reflect.TypeOf(decl).String() == "*ast.FuncDecl" { 77 | funcDecl := decl.(*ast.FuncDecl) 78 | // Only interested in main() function 79 | if funcDecl.Name.Name != "main" { 80 | continue 81 | } 82 | 83 | for _, decl2 := range funcDecl.Body.List { 84 | if reflect.TypeOf(decl2).String() == "*ast.AssignStmt" { 85 | assignToOriginUnit := decl2.(*ast.AssignStmt) 86 | for _, ouVariable := range assignToOriginUnit.Lhs { 87 | ident := ouVariable.(*ast.Ident) 88 | if ident.Obj.Name == "originUnit" { 89 | isAssigningToOriginUnit = true 90 | } 91 | } 92 | for _, ouValue := range assignToOriginUnit.Rhs { 93 | if reflect.TypeOf(ouValue).String() == "*ast.CallExpr" { 94 | v1 := ouValue.(*ast.CallExpr) 95 | v2 := v1.Fun.(*ast.SelectorExpr) 96 | v3 := v2.X.(*ast.Ident) 97 | if v3.Name == "strings" && v2.Sel.Name == "ToUpper" { 98 | isAssigningToOriginUnitFromCorrectFunction = true 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | if !isAssigningToOriginUnit || !isAssigningToOriginUnitFromCorrectFunction { 106 | t.Error("Did not assign proper value to originUnit") 107 | } 108 | } 109 | } 110 | } 111 | 112 | func TestReadsCurrentTemperatureM2(t *testing.T) { 113 | t.Run("reads current temperature", func(t *testing.T) { 114 | dir, err := os.Getwd() 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | 119 | cmd := exec.Command(path.Join(dir, binaryName), []string{"C"}...) 120 | 121 | stdin, e := cmd.StdinPipe() 122 | if e != nil { 123 | panic(e) 124 | } 125 | stderr, e := cmd.StderrPipe() 126 | if e != nil { 127 | panic(e) 128 | } 129 | 130 | if e := cmd.Start(); e != nil { 131 | panic(e) 132 | } 133 | _, e = stdin.Write([]byte("FFFF\n")) 134 | if e != nil { 135 | panic(e) 136 | } 137 | _, e = stdin.Write([]byte("n\n")) 138 | if e != nil { 139 | panic(e) 140 | } 141 | stdin.Close() 142 | errPrint, _ := ioutil.ReadAll(stderr) 143 | if !strings.Contains(string(errPrint), errReadingInput.Error()) { 144 | t.Fatal("Did not print errReadingInput error") 145 | } 146 | }) 147 | } 148 | 149 | func TestCheckConversionM2(t *testing.T) { 150 | t.Run("converts temperature", func(t *testing.T) { 151 | dir, err := os.Getwd() 152 | if err != nil { 153 | t.Fatal(err) 154 | } 155 | 156 | cmd := exec.Command(path.Join(dir, binaryName), []string{"F"}...) 157 | 158 | stdin, e := cmd.StdinPipe() 159 | if e != nil { 160 | panic(e) 161 | } 162 | outPipe, e := cmd.StdoutPipe() 163 | if e != nil { 164 | panic(e) 165 | } 166 | 167 | if e := cmd.Start(); e != nil { 168 | panic(e) 169 | } 170 | _, e = stdin.Write([]byte("32\n")) 171 | if e != nil { 172 | panic(e) 173 | } 174 | _, e = stdin.Write([]byte("n\n")) 175 | if e != nil { 176 | panic(e) 177 | } 178 | stdin.Close() 179 | outPrint, _ := ioutil.ReadAll(outPipe) 180 | if !strings.Contains(string(outPrint), "32 F = 0 C") { 181 | t.Fatal("Did not properly convert temperature") 182 | } 183 | }) 184 | 185 | } 186 | 187 | func TestPromptAgainM2(t *testing.T) { 188 | t.Run("convert again", func(t *testing.T) { 189 | dir, err := os.Getwd() 190 | if err != nil { 191 | t.Fatal(err) 192 | } 193 | 194 | cmd := exec.Command(path.Join(dir, binaryName), []string{"F"}...) 195 | 196 | stdin, e := cmd.StdinPipe() 197 | if e != nil { 198 | panic(e) 199 | } 200 | outPipe, e := cmd.StdoutPipe() 201 | if e != nil { 202 | panic(e) 203 | } 204 | 205 | if e := cmd.Start(); e != nil { 206 | panic(e) 207 | } 208 | _, e = stdin.Write([]byte("32\n")) 209 | if e != nil { 210 | panic(e) 211 | } 212 | _, e = stdin.Write([]byte(" Y\n")) 213 | if e != nil { 214 | panic(e) 215 | } 216 | _, e = stdin.Write([]byte("40\n")) 217 | if e != nil { 218 | panic(e) 219 | } 220 | stdin.Close() 221 | outPrint, _ := ioutil.ReadAll(outPipe) 222 | if !strings.Contains(string(outPrint), "32 F = 0 C") || 223 | !strings.Contains(string(outPrint), "40 F = 4 C") { 224 | t.Fatal("Did not prompt user for another run") 225 | } 226 | 227 | }) 228 | } 229 | 230 | func TestParsePromptToUpperM2(t *testing.T) { 231 | t.Run("prompt to upper", func(t *testing.T) { 232 | dir, err := os.Getwd() 233 | if err != nil { 234 | t.Fatal(err) 235 | } 236 | 237 | cmd := exec.Command(path.Join(dir, binaryName), []string{"F"}...) 238 | 239 | stdin, e := cmd.StdinPipe() 240 | if e != nil { 241 | panic(e) 242 | } 243 | outPipe, e := cmd.StdoutPipe() 244 | if e != nil { 245 | panic(e) 246 | } 247 | 248 | if e := cmd.Start(); e != nil { 249 | panic(e) 250 | } 251 | _, e = stdin.Write([]byte("32\n")) 252 | if e != nil { 253 | panic(e) 254 | } 255 | _, e = stdin.Write([]byte("y\n")) 256 | if e != nil { 257 | panic(e) 258 | } 259 | _, e = stdin.Write([]byte("212\n")) 260 | if e != nil { 261 | panic(e) 262 | } 263 | stdin.Close() 264 | outPrint, _ := ioutil.ReadAll(outPipe) 265 | if !strings.Contains(string(outPrint), "32 F = 0 C") || 266 | !strings.Contains(string(outPrint), "212 F = 100 C") { 267 | t.Fatal("Did not properly parse user input") 268 | } 269 | }) 270 | } 271 | --------------------------------------------------------------------------------