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  }