github.com/study-group-99/pilates@v0.2.2/libft.go (about) 1 package pilates 2 3 import ( 4 "bufio" 5 "embed" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "regexp" 12 "strings" 13 14 "github.com/bh90210/clir" 15 "github.com/leaanthony/spinner" 16 "github.com/study-group-99/pilates/internal" 17 ) 18 19 const ( 20 libftDescription = "Install and run unit tests, benchmarks, norm check, makefile check & memory leaks checks." 21 libftLongDescription = "\nExamples:\n # Init pilates for your libft.\n pilates libft init\n\n # Run unit tests and generate a report.\n pilates libft run --unit --report" 22 23 libftInitDescription = "Generates the unit tests under default folder 'pilates', two CMake files on root level and a .gitignore. You can edit the tests but DO NOT rename or delete anything unless you know what you are doing." 24 libftInitLongDescription = "\nExamples:\n # Run init\n pilates libft init\n\n # Run init with -f, --force option\n pilates libft init -f" 25 libftInitForce = "Forces files gerenation." 26 libftInitError = `initialization is not complete! 27 28 pilates detected the usage of parameter name 'new' in the above functions. 29 Our unit testing is written in C++ thus keyword 'new' can not be be used as argument name. 30 Changing the above lines and equivalent functions is OK with Moulinette. 31 32 If you try to run the tests anyway you will get an error. Please change 'new' to 'neew' or anything else. 33 pilates can do this for you automagically by passing the '--fix-new' option like so 'pilates libft init --fix-new' 34 35 https://stackoverflow.com/questions/20653245/error-in-compiling-c-code-with-variable-name-new-with-g 36 ` 37 38 libftRunDescription = "Runs tests with the options provided via flags. You need to include at least one (-r, --report flag not included)." 39 libftRunLongDescription = "\nExamples:\n # Run unit tests with benchmarks.\n pilates libft run -ub\n\n # Run unit tests with linter test and generates a report.\n pilates libft run -ulr\n\n # Pass verbose options.\n pilates libft run --unit --norm" 40 libftRunAll = "Runs all tests." 41 libftRunUnit = "Runs unit tests. Cmake is necessary." 42 libftRunCoverage = "Prints coverage for your library. Gcov is necessary." 43 libftRunBenchmark = "Runs bernchamarks against your library. Cmake is necessary." 44 libftRunMakefile = "Checks 'Makefile' for compliance." 45 libftRunNorm = "Runs linter checks. Norminette is necessary." 46 libftRunLeaks = "Runs memory leaks tests. Valgrind is necessary." 47 libftRunReport = "Generates a 'report.txt' with the results." 48 ) 49 50 //go:embed libft/* 51 var libftTests embed.FS 52 53 type libft struct { 54 *clir.Command 55 } 56 57 // LibftCommand takes a *clir.Cli argument and setsup 'libft' subcommand. 58 func LibftCommand(cli *clir.Cli) { 59 libft := &libft{cli.NewSubCommand("libft", libftDescription)} 60 libft.LongDescription(libftLongDescription) 61 62 // pilates libft subcommands 63 libft.init() 64 libft.run() 65 } 66 67 func (l *libft) init() { 68 init := l.NewSubCommand("init", libftInitDescription) 69 init.LongDescription(libftInitLongDescription) 70 71 // pilates libft init --flags 72 var forceInit bool 73 init.BoolFlag("force", "f", libftInitForce, &forceInit) 74 var fixNew bool 75 init.BoolFlag("fix-new", "", "If your 'libft.h' contains any parameter named 'new' this option will change it to 'n' along with the corresponding 'ft_*.c' file.", &fixNew) 76 77 init.Action(func() error { 78 var path string = "pilates" 79 switch { 80 case forceInit && fixNew: 81 return fmt.Errorf("the -f, --force and --fix-new options are indented to be used separately") 82 case fixNew: 83 // apply 'new' fix 84 return newFix() 85 } 86 87 // check if folder pilates exists 88 _, err := os.Stat(path) 89 if !os.IsNotExist(err) && !forceInit { 90 return fmt.Errorf("directory %s already exists. If know what you are doing try the -f, --force option", path) 91 } 92 93 // initialize libft 94 err = libftInit(path) 95 if err != nil { 96 return err 97 } 98 99 // check for 'new' in header 100 newPresense, err := internal.NewExists() 101 if err != nil { 102 return err 103 } 104 if newPresense { 105 return fmt.Errorf(libftInitError) 106 } 107 108 fmt.Println("Ready!") 109 return nil 110 }) 111 } 112 113 func (l *libft) run() { 114 run := l.NewSubCommand("run", libftRunDescription) 115 run.LongDescription(libftRunLongDescription) 116 117 // pilates libft run --flags 118 var all bool 119 run.BoolFlag("all", "a", libftRunAll, &all) 120 var unit bool 121 run.BoolFlag("unit", "u", libftRunUnit, &unit) 122 var coverage bool 123 run.BoolFlag("coverage", "c", libftRunCoverage, &coverage) 124 var bench bool 125 run.BoolFlag("benchmark", "b", libftRunBenchmark, &bench) 126 var makefile bool 127 run.BoolFlag("makefile", "m", libftRunMakefile, &makefile) 128 var norm bool 129 run.BoolFlag("norm", "n", libftRunNorm, &norm) 130 var leaks bool 131 run.BoolFlag("leaks", "l", libftRunLeaks, &leaks) 132 var report bool 133 run.BoolFlag("report", "r", libftRunReport, &report) 134 135 run.Action(func() error { 136 137 // check for 'new' in header 138 newPresense, err := internal.NewExists() 139 if err != nil { 140 return fmt.Errorf("wrong directory") 141 } 142 143 if newPresense { 144 return fmt.Errorf(libftInitError) 145 } 146 147 // if --all flag is used set true every other var/flag 148 switch { 149 case all: 150 unit = true 151 coverage = true 152 bench = true 153 leaks = true 154 makefile = true 155 norm = true 156 report = true 157 // if no flags are used return error 158 case !unit && !coverage && !bench && !leaks && !makefile && !norm: 159 return fmt.Errorf("error: must specify at least one option (not including --report))\nrun 'pilates libft run -h' for help") 160 } 161 162 var file *os.File 163 if report { 164 file, err = os.Create("report.txt") 165 if err != nil { 166 fmt.Printf("error: %s\n", err) 167 } 168 169 defer file.Close() 170 defer fmt.Println("generated report file 'report.txt' successfully.") 171 } 172 173 if unit { 174 unitTest(report, file) 175 } 176 177 if makefile { 178 makefileCheck(report, file) 179 } 180 181 if norm { 182 normCheck(report, file) 183 } 184 185 if leaks || bench { 186 fmt.Println("WIP. We need your help implementing memory leaks and benchmarks! go to https://github.com/study-group-99/pilates/discussions for more information.") 187 } 188 189 if coverage { 190 coverageCheck(report, file) 191 } 192 193 return nil 194 }) 195 } 196 197 func newFix() error { 198 fmt.Println("checking your libft.h") 199 header, err := os.OpenFile("libft.h", os.O_APPEND, os.ModeAppend) 200 if err != nil { 201 return err 202 } 203 204 changeLines := make([]string, 0) 205 scanner := bufio.NewScanner(header) 206 for scanner.Scan() { 207 if strings.Contains(scanner.Text(), " new") || strings.Contains(scanner.Text(), "*new") || 208 strings.Contains(scanner.Text(), "\tnew") { 209 fmt.Println("found function", scanner.Text()) 210 changeLines = append(changeLines, scanner.Text()) 211 } 212 } 213 214 header.Close() 215 216 if len(changeLines) == 0 { 217 return fmt.Errorf("found no 'new' use. nothing to be done all clean") 218 } 219 220 changeLines = append(changeLines, "libft.h") 221 222 fmt.Println("\"cleaning\" your files") 223 for _, value := range changeLines { 224 var name string 225 // extract function's name 226 if value == "libft.h" { 227 name = "libft.h" 228 } else { 229 // create a regular expresion to retrieve the name of the function 230 // in the form 'ft_some_function(' 231 r := regexp.MustCompile(`([a-zA-Z]+(_[a-zA-Z]+)+)\(`) 232 // actually run the regex query then trim the '(' on the right and append a '.c' to the name 233 name = fmt.Sprintf("%s.c", strings.TrimRight(r.FindAllString(value, -1)[0], "(")) 234 } 235 // open file 236 ft, err := os.ReadFile(name) 237 if err != nil { 238 return err 239 } 240 // replace 'new' 241 new := strings.ReplaceAll(string(ft), " new", " neew") 242 new = strings.ReplaceAll(new, "*new", "*neew") 243 new = strings.ReplaceAll(new, "\tnew", "\tneew") 244 new = strings.ReplaceAll(new, "!new", "!neew") 245 // write it 246 err = ioutil.WriteFile(name, []byte(new), 0) 247 if err != nil { 248 return err 249 } 250 fmt.Println(name, "done.") 251 } 252 253 fmt.Println("All done! Now you can run 'pilates libft run -u'") 254 return nil 255 } 256 257 func libftInit(path string) error { 258 fmt.Println("pilates libft initialization") 259 260 // try remove the 'build' folder 261 // this needs to be done to remove previous builds artifacts 262 os.RemoveAll("build") 263 264 // if not, or if -f option is used create it 265 os.Mkdir(path, 0744) 266 dir, err := libftTests.ReadDir("libft") 267 if err != nil { 268 return err 269 } 270 271 for _, file := range dir { 272 data, err := libftTests.ReadFile(fmt.Sprintf("libft/%s", file.Name())) 273 if err != nil { 274 return err 275 } 276 277 if file.Name() == "CMakeLists.txt.test" { 278 err = ioutil.WriteFile(fmt.Sprintf("%s/%s", path, "CMakeLists.txt"), data, 0755) 279 if err != nil { 280 return err 281 } 282 continue 283 } 284 285 if file.Name() == "CMakeLists.txt" || file.Name() == "CMakeLists.txt.in" { 286 err = ioutil.WriteFile(file.Name(), data, 0755) 287 if err != nil { 288 return err 289 } 290 continue 291 } 292 293 if file.Name() == "gitignore" { 294 // check if .gitignore is already present 295 _, err := os.Stat(".gitignore") 296 // if not create one 297 if os.IsNotExist(err) { 298 err = ioutil.WriteFile(".gitignore", data, 0755) 299 if err != nil { 300 return err 301 } 302 continue 303 } 304 305 // if it exists open it check if what we want to exclude is already present 306 // if not append it. 307 gitignore, err := os.ReadFile(".gitignore") 308 if err != nil { 309 return err 310 } 311 312 ignoreList := []string{"build", "pilates", "*.txt", "*.in", "*.cpp"} 313 for _, key := range ignoreList { 314 if !strings.Contains(string(gitignore), key) { 315 gitignore = []byte(fmt.Sprintf("%s\n%s", string(gitignore), key)) 316 } 317 } 318 319 // end file with a new line 320 gitignore = []byte(fmt.Sprintf("%s\n", string(gitignore))) 321 322 err = os.WriteFile(".gitignore", []byte(gitignore), os.ModeAppend) 323 if err != nil { 324 return err 325 } 326 continue 327 } 328 329 err = ioutil.WriteFile(fmt.Sprintf("%s/%s", path, file.Name()), data, 0755) 330 if err != nil { 331 return err 332 } 333 } 334 335 return nil 336 } 337 338 func unitTest(report bool, file *os.File) { 339 cmd := exec.Command("cmake", "-S", ".", "-B", "build") 340 genSpinner := spinner.New("Generating build") 341 genSpinner.Start() 342 cmd.Env = os.Environ() 343 if err := cmd.Run(); err != nil { 344 genSpinner.Error(err.Error()) 345 } else { 346 genSpinner.Success() 347 } 348 349 cmd = exec.Command("cmake", "--build", "build") 350 buildSpinner := spinner.New("Building C++ files") 351 buildSpinner.Start() 352 cmd.Stderr = os.Stderr 353 cmd.Env = os.Environ() 354 if err := cmd.Run(); err != nil { 355 buildSpinner.Error(err.Error()) 356 } else { 357 buildSpinner.Success() 358 } 359 360 cmd = exec.Command("ctest", "--output-on-failure") 361 cmd.Stderr = os.Stderr 362 cmd.Env = os.Environ() 363 if report { 364 cmd.Stdout = io.MultiWriter(os.Stdout, file) 365 } else { 366 cmd.Stdout = os.Stdout 367 } 368 369 os.Chdir("build") 370 if err := cmd.Run(); err != nil { 371 fmt.Printf("error: %s\n", err) 372 } 373 os.Chdir("..") 374 } 375 376 func normCheck(report bool, file *os.File) { 377 cmd := exec.Command("norminette") 378 cmd.Args = append(cmd.Args, "libft.h") 379 dir, err := os.ReadDir("./") 380 if err != nil { 381 fmt.Printf("error: %s\n", err) 382 } 383 384 for _, file := range dir { 385 if strings.Contains(file.Name(), "ft_") { 386 cmd.Args = append(cmd.Args, file.Name()) 387 } 388 } 389 cmd.Stdout = os.Stdout 390 cmd.Stderr = os.Stderr 391 cmd.Env = os.Environ() 392 if report { 393 cmd.Stdout = io.MultiWriter(os.Stdout, file) 394 } else { 395 cmd.Stdout = os.Stdout 396 } 397 if err := cmd.Run(); err != nil { 398 fmt.Printf("error: %s\n", err) 399 } 400 } 401 402 func makefileCheck(report bool, file *os.File) { 403 fmt.Println("makefile checks") 404 makeVariations := []string{"all", "clean", "libft.a", "re", "fclean", "bonus"} 405 for _, val := range makeVariations { 406 cmd := exec.Command("make", val) 407 fmt.Printf("make: %s\n", val) 408 cmd.Env = os.Environ() 409 if report { 410 file.WriteString(fmt.Sprintf("make: %s\n", val)) 411 cmd.Stderr = io.MultiWriter(os.Stderr, file) 412 } else { 413 cmd.Stderr = os.Stderr 414 } 415 if err := cmd.Run(); err != nil { 416 fmt.Printf("error: %s\n", err) 417 } else { 418 fmt.Printf("make: %s - Passed\n", val) 419 file.WriteString(fmt.Sprintf("make: %s - Passed\n", val)) 420 } 421 } 422 423 // clean everything in the end 424 cmd := exec.Command("make", "fclean") 425 cmd.Run() 426 } 427 428 func coverageCheck(report bool, file *os.File) { 429 cmd := exec.Command("gcovr", "--exclude", "'.*test.*'", "--root", ".") 430 cmd.Stderr = os.Stderr 431 cmd.Env = os.Environ() 432 if report { 433 cmd.Stdout = io.MultiWriter(os.Stdout, file) 434 } else { 435 cmd.Stdout = os.Stdout 436 } 437 if err := cmd.Run(); err != nil { 438 fmt.Printf("error: %s\n", err) 439 } 440 }