gitee.com/mirrors/gauge@v1.0.6/refactor/refactor.go (about) 1 // Copyright 2015 ThoughtWorks, Inc. 2 3 // This file is part of Gauge. 4 5 // Gauge is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 10 // Gauge is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU General Public License for more details. 14 15 // You should have received a copy of the GNU General Public License 16 // along with Gauge. If not, see <http://www.gnu.org/licenses/>. 17 18 /* 19 Given old and new step gives the filenames of specification, concepts and files in code changed. 20 21 Refactoring Flow: 22 - Refactor specs and concepts in memory 23 - Checks if it is a concept or not 24 - In case of concept - writes to file and skips the runner 25 - If its not a concept (its a step) - need to know the text, so makes a call to runner to get the text(step name) 26 - Refactors the text(changes param positions ect) and sends it to runner to refactor implementations. 27 */ 28 package refactor 29 30 import ( 31 "errors" 32 "fmt" 33 "os" 34 "path/filepath" 35 "strings" 36 37 "github.com/getgauge/gauge/config" 38 "github.com/getgauge/gauge/formatter" 39 "github.com/getgauge/gauge/gauge" 40 "github.com/getgauge/gauge/gauge_messages" 41 "github.com/getgauge/gauge/logger" 42 "github.com/getgauge/gauge/parser" 43 "github.com/getgauge/gauge/runner" 44 "github.com/getgauge/gauge/util" 45 ) 46 47 type rephraseRefactorer struct { 48 oldStep *gauge.Step 49 newStep *gauge.Step 50 isConcept bool 51 runner runner.Runner 52 } 53 54 type refactoringResult struct { 55 Success bool 56 SpecsChanged []*gauge_messages.FileChanges 57 ConceptsChanged []*gauge_messages.FileChanges 58 RunnerFilesChanged []*gauge_messages.FileChanges 59 Errors []string 60 Warnings []string 61 } 62 63 func (refactoringResult *refactoringResult) String() string { 64 result := fmt.Sprintf("Refactoring result from gauge:\n") 65 result += fmt.Sprintf("Specs changed : %s\n", refactoringResult.specFilesChanged()) 66 result += fmt.Sprintf("Concepts changed : %s\n", refactoringResult.conceptFilesChanged()) 67 result += fmt.Sprintf("Source files changed : %s\n", refactoringResult.runnerFilesChanged()) 68 result += fmt.Sprintf("Warnings : %s\n", refactoringResult.Warnings) 69 return result 70 } 71 72 func (refactoringResult *refactoringResult) appendWarnings(warnings []*parser.Warning) { 73 if refactoringResult.Warnings == nil { 74 refactoringResult.Warnings = make([]string, 0) 75 } 76 for _, warning := range warnings { 77 refactoringResult.Warnings = append(refactoringResult.Warnings, warning.Message) 78 } 79 } 80 81 func (refactoringResult *refactoringResult) AllFilesChanged() []string { 82 filesChanged := make([]string, 0) 83 filesChanged = append(filesChanged, refactoringResult.specFilesChanged()...) 84 filesChanged = append(filesChanged, refactoringResult.conceptFilesChanged()...) 85 filesChanged = append(filesChanged, refactoringResult.runnerFilesChanged()...) 86 return filesChanged 87 } 88 89 func (refactoringResult *refactoringResult) conceptFilesChanged() []string { 90 filesChanged := make([]string, 0) 91 for _, fileChange := range refactoringResult.ConceptsChanged { 92 filesChanged = append(filesChanged, fileChange.FileName) 93 } 94 return filesChanged 95 } 96 97 func (refactoringResult *refactoringResult) specFilesChanged() []string { 98 filesChanged := make([]string, 0) 99 for _, filesChange := range refactoringResult.SpecsChanged { 100 filesChanged = append(filesChanged, filesChange.FileName) 101 } 102 return filesChanged 103 } 104 105 func (refactoringResult *refactoringResult) runnerFilesChanged() []string { 106 filesChanged := make([]string, 0) 107 for _, fileChange := range refactoringResult.RunnerFilesChanged { 108 filesChanged = append(filesChanged, fileChange.FileName) 109 } 110 return filesChanged 111 } 112 113 // PerformRephraseRefactoring given an old step and new step refactors specs and concepts in memory and if its a concept writes to file 114 // else invokes runner to get the step name and refactors the step and sends it to runner to refactor implementation. 115 func PerformRephraseRefactoring(oldStep, newStep string, startChan *runner.StartChannels, specDirs []string) *refactoringResult { 116 defer killRunner(startChan) 117 if newStep == oldStep { 118 return &refactoringResult{Success: true} 119 } 120 agent, errs := getRefactorAgent(oldStep, newStep, nil) 121 122 if len(errs) > 0 { 123 var messages []string 124 for _, err := range errs { 125 messages = append(messages, err.Error()) 126 } 127 return rephraseFailure(messages...) 128 } 129 result, specs, conceptDictionary := parseSpecsAndConcepts(specDirs) 130 if !result.Success { 131 return result 132 } 133 134 refactorResult := agent.performRefactoringOn(specs, conceptDictionary, startChan) 135 refactorResult.Warnings = append(refactorResult.Warnings, result.Warnings...) 136 return refactorResult 137 } 138 139 // GetRefactoringChanges given an old step and new step gives the list of steps that need to be changed to perform refactoring. 140 // It also provides the changes to be made on the implementation files. 141 func GetRefactoringChanges(oldStep, newStep string, runner runner.Runner, specDirs []string) *refactoringResult { 142 if newStep == oldStep { 143 return &refactoringResult{Success: true} 144 } 145 agent, errs := getRefactorAgent(oldStep, newStep, runner) 146 147 if len(errs) > 0 { 148 var messages []string 149 for _, err := range errs { 150 messages = append(messages, err.Error()) 151 } 152 return rephraseFailure(messages...) 153 } 154 result, specs, conceptDictionary := parseSpecsAndConcepts(specDirs) 155 if !result.Success { 156 return result 157 } 158 159 refactorResult := agent.getRefactoringChangesFor(specs, conceptDictionary) 160 refactorResult.Warnings = append(refactorResult.Warnings, result.Warnings...) 161 return refactorResult 162 } 163 164 func parseSpecsAndConcepts(specDirs []string) (*refactoringResult, []*gauge.Specification, *gauge.ConceptDictionary) { 165 result := &refactoringResult{Success: true, Errors: make([]string, 0), Warnings: make([]string, 0)} 166 167 var specs []*gauge.Specification 168 var specParseResults []*parser.ParseResult 169 170 for _, dir := range specDirs { 171 specFiles := util.GetSpecFiles([]string{filepath.Join(config.ProjectRoot, dir)}) 172 specSlice, specParseResultsSlice := parser.ParseSpecFiles(specFiles, &gauge.ConceptDictionary{}, gauge.NewBuildErrors()) 173 specs = append(specs, specSlice...) 174 specParseResults = append(specParseResults, specParseResultsSlice...) 175 } 176 177 addErrorsAndWarningsToRefactoringResult(result, specParseResults...) 178 if !result.Success { 179 return result, nil, nil 180 } 181 182 conceptDictionary, parseResult, err := parser.CreateConceptsDictionary() 183 if err != nil { 184 return rephraseFailure(err.Error()), nil, nil 185 } 186 addErrorsAndWarningsToRefactoringResult(result, parseResult) 187 return result, specs, conceptDictionary 188 } 189 190 func killRunner(startChan *runner.StartChannels) { 191 startChan.KillChan <- true 192 } 193 194 func rephraseFailure(errors ...string) *refactoringResult { 195 return &refactoringResult{Success: false, Errors: errors} 196 } 197 198 func addErrorsAndWarningsToRefactoringResult(refactorResult *refactoringResult, parseResults ...*parser.ParseResult) { 199 for _, parseResult := range parseResults { 200 if !parseResult.Ok { 201 refactorResult.Success = false 202 for _, err := range parseResult.Errors() { 203 refactorResult.Errors = append(refactorResult.Errors, err) 204 } 205 } 206 refactorResult.appendWarnings(parseResult.Warnings) 207 } 208 } 209 210 func (agent *rephraseRefactorer) performRefactoringOn(specs []*gauge.Specification, conceptDictionary *gauge.ConceptDictionary, startChan *runner.StartChannels) *refactoringResult { 211 specsRefactored, conceptFilesRefactored := agent.rephraseInSpecsAndConcepts(&specs, conceptDictionary) 212 var runner runner.Runner 213 select { 214 case runner = <-startChan.RunnerChan: 215 case err := <-startChan.ErrorChan: 216 logger.Debugf(true, "Cannot perform refactoring: Unable to connect to runner."+err.Error()) 217 return &refactoringResult{Success: false, Errors: make([]string, 0), Warnings: make([]string, 0)} 218 } 219 agent.runner = runner 220 result := agent.refactorStepImplementations(true) 221 if !result.Success { 222 return result 223 } 224 result.SpecsChanged, result.ConceptsChanged = getFileChanges(specs, conceptDictionary, specsRefactored, conceptFilesRefactored) 225 writeFileChangesToDisk(result) 226 return result 227 } 228 229 func writeFileChangesToDisk(result *refactoringResult) { 230 for _, fileChange := range result.SpecsChanged { 231 util.SaveFile(fileChange.FileName, fileChange.FileContent, true) 232 } 233 for _, fileChange := range result.ConceptsChanged { 234 util.SaveFile(fileChange.FileName, fileChange.FileContent, true) 235 } 236 } 237 238 func (agent *rephraseRefactorer) getRefactoringChangesFor(specs []*gauge.Specification, conceptDictionary *gauge.ConceptDictionary) *refactoringResult { 239 specsRefactored, conceptFilesRefactored := agent.rephraseInSpecsAndConcepts(&specs, conceptDictionary) 240 result := agent.refactorStepImplementations(false) 241 if !result.Success { 242 return result 243 } 244 result.SpecsChanged, result.ConceptsChanged = getFileChanges(specs, conceptDictionary, specsRefactored, conceptFilesRefactored) 245 return result 246 } 247 248 func (agent *rephraseRefactorer) refactorStepImplementations(shouldSaveChanges bool) *refactoringResult { 249 result := &refactoringResult{Success: false, Errors: make([]string, 0), Warnings: make([]string, 0)} 250 if !agent.isConcept { 251 stepName, err, warning := agent.getStepNameFromRunner(agent.runner) 252 if err != nil { 253 result.Errors = append(result.Errors, err.Error()) 254 return result 255 } 256 if warning == nil { 257 runnerFilesChanged, err := agent.requestRunnerForRefactoring(agent.runner, stepName, shouldSaveChanges) 258 if err != nil { 259 result.Errors = append(result.Errors, fmt.Sprintf("Cannot perform refactoring: %s", err)) 260 return result 261 } 262 result.RunnerFilesChanged = runnerFilesChanged 263 } else { 264 result.Warnings = append(result.Warnings, warning.Message) 265 } 266 } 267 result.Success = true 268 return result 269 } 270 271 func (agent *rephraseRefactorer) rephraseInSpecsAndConcepts(specs *[]*gauge.Specification, conceptDictionary *gauge.ConceptDictionary) (map[*gauge.Specification][]*gauge.StepDiff, map[string][]*gauge.StepDiff) { 272 specsRefactored := make(map[*gauge.Specification][]*gauge.StepDiff, 0) 273 conceptsRefactored := make(map[string][]*gauge.StepDiff, 0) 274 orderMap := agent.createOrderOfArgs() 275 for _, spec := range *specs { 276 diffs, isRefactored := spec.RenameSteps(*agent.oldStep, *agent.newStep, orderMap) 277 if isRefactored { 278 specsRefactored[spec] = diffs 279 } 280 } 281 isConcept := false 282 for _, concept := range conceptDictionary.ConceptsMap { 283 isRefactored := false 284 for _, item := range concept.ConceptStep.Items { 285 if item.Kind() == gauge.StepKind { 286 diff, isRefactored := item.(*gauge.Step).Rename(*agent.oldStep, *agent.newStep, isRefactored, orderMap, &isConcept) 287 if isRefactored { 288 conceptsRefactored[concept.FileName] = append(conceptsRefactored[concept.FileName], diff) 289 } 290 } 291 } 292 } 293 agent.isConcept = isConcept 294 return specsRefactored, conceptsRefactored 295 } 296 297 func (agent *rephraseRefactorer) createOrderOfArgs() map[int]int { 298 orderMap := make(map[int]int, len(agent.newStep.Args)) 299 for i, arg := range agent.newStep.Args { 300 orderMap[i] = SliceIndex(len(agent.oldStep.Args), func(i int) bool { return agent.oldStep.Args[i].String() == arg.String() }) 301 } 302 return orderMap 303 } 304 305 // SliceIndex gives the index of the args. 306 func SliceIndex(limit int, predicate func(i int) bool) int { 307 for i := 0; i < limit; i++ { 308 if predicate(i) { 309 return i 310 } 311 } 312 return -1 313 } 314 315 func getRefactorAgent(oldStepText, newStepText string, runner runner.Runner) (*rephraseRefactorer, []parser.ParseError) { 316 specParser := new(parser.SpecParser) 317 stepTokens, errs := specParser.GenerateTokens("* "+oldStepText+"\n"+"*"+newStepText, "") 318 if len(errs) > 0 { 319 return nil, errs 320 } 321 322 steps := make([]*gauge.Step, 0) 323 for _, stepToken := range stepTokens { 324 step, parseRes := parser.CreateStepUsingLookup(stepToken, nil, "") 325 if parseRes != nil && len(parseRes.ParseErrors) > 0 { 326 return nil, parseRes.ParseErrors 327 } 328 steps = append(steps, step) 329 } 330 return &rephraseRefactorer{oldStep: steps[0], newStep: steps[1], runner: runner}, []parser.ParseError{} 331 } 332 333 func (agent *rephraseRefactorer) requestRunnerForRefactoring(testRunner runner.Runner, stepName string, shouldSaveChanges bool) ([]*gauge_messages.FileChanges, error) { 334 refactorRequest, err := agent.createRefactorRequest(testRunner, stepName, shouldSaveChanges) 335 if err != nil { 336 return nil, err 337 } 338 refactorResponse := agent.sendRefactorRequest(testRunner, refactorRequest) 339 var runnerError error 340 if !refactorResponse.GetSuccess() { 341 logger.Errorf(false, "Refactoring error response from runner: %v", refactorResponse.GetError()) 342 runnerError = errors.New(refactorResponse.GetError()) 343 } 344 if len(refactorResponse.GetFileChanges()) == 0 { 345 for _, file := range refactorResponse.GetFilesChanged() { 346 refactorResponse.FileChanges = append(refactorResponse.FileChanges, &gauge_messages.FileChanges{FileName: file}) 347 } 348 } 349 return refactorResponse.GetFileChanges(), runnerError 350 } 351 352 func (agent *rephraseRefactorer) sendRefactorRequest(testRunner runner.Runner, refactorRequest *gauge_messages.Message) *gauge_messages.RefactorResponse { 353 response, err := testRunner.ExecuteMessageWithTimeout(refactorRequest) 354 if err != nil { 355 return &gauge_messages.RefactorResponse{Success: false, Error: err.Error()} 356 } 357 return response.GetRefactorResponse() 358 } 359 360 //Todo: Check for inline tables 361 func (agent *rephraseRefactorer) createRefactorRequest(runner runner.Runner, stepName string, shouldSaveChanges bool) (*gauge_messages.Message, error) { 362 oldStepValue, err := agent.getStepValueFor(agent.oldStep, stepName) 363 if err != nil { 364 return nil, err 365 } 366 orderMap := agent.createOrderOfArgs() 367 newStepName := agent.generateNewStepName(oldStepValue.Args, orderMap) 368 newStepValue, err := parser.ExtractStepValueAndParams(newStepName, false) 369 if err != nil { 370 return nil, err 371 } 372 oldProtoStepValue := gauge.ConvertToProtoStepValue(oldStepValue) 373 newProtoStepValue := gauge.ConvertToProtoStepValue(newStepValue) 374 return &gauge_messages.Message{MessageType: gauge_messages.Message_RefactorRequest, 375 RefactorRequest: &gauge_messages.RefactorRequest{ 376 OldStepValue: oldProtoStepValue, 377 NewStepValue: newProtoStepValue, 378 ParamPositions: agent.createParameterPositions(orderMap), 379 SaveChanges: shouldSaveChanges, 380 }, 381 }, nil 382 } 383 384 func (agent *rephraseRefactorer) generateNewStepName(args []string, orderMap map[int]int) string { 385 agent.newStep.PopulateFragments() 386 paramIndex := 0 387 for _, fragment := range agent.newStep.Fragments { 388 if fragment.GetFragmentType() == gauge_messages.Fragment_Parameter { 389 if orderMap[paramIndex] != -1 { 390 fragment.GetParameter().Value = args[orderMap[paramIndex]] 391 } 392 paramIndex++ 393 } 394 } 395 return parser.ConvertToStepText(agent.newStep.Fragments) 396 } 397 398 func (agent *rephraseRefactorer) getStepNameFromRunner(runner runner.Runner) (string, error, *parser.Warning) { 399 stepNameMessage := &gauge_messages.Message{MessageType: gauge_messages.Message_StepNameRequest, StepNameRequest: &gauge_messages.StepNameRequest{StepValue: agent.oldStep.Value}} 400 responseMessage, err := runner.ExecuteMessageWithTimeout(stepNameMessage) 401 if err != nil { 402 return "", err, nil 403 } 404 if !(responseMessage.GetStepNameResponse().GetIsStepPresent()) { 405 return "", nil, &parser.Warning{Message: fmt.Sprintf("Step implementation not found: %s", agent.oldStep.LineText)} 406 } 407 if responseMessage.GetStepNameResponse().GetHasAlias() { 408 return "", fmt.Errorf("steps with aliases : '%s' cannot be refactored", strings.Join(responseMessage.GetStepNameResponse().GetStepName(), "', '")), nil 409 } 410 return responseMessage.GetStepNameResponse().GetStepName()[0], nil, nil 411 } 412 413 func (agent *rephraseRefactorer) createParameterPositions(orderMap map[int]int) []*gauge_messages.ParameterPosition { 414 paramPositions := make([]*gauge_messages.ParameterPosition, 0) 415 for k, v := range orderMap { 416 paramPositions = append(paramPositions, &gauge_messages.ParameterPosition{NewPosition: int32(k), OldPosition: int32(v)}) 417 } 418 return paramPositions 419 } 420 421 func (agent *rephraseRefactorer) getStepValueFor(step *gauge.Step, stepName string) (*gauge.StepValue, error) { 422 return parser.ExtractStepValueAndParams(stepName, false) 423 } 424 425 func createDiffs(diffs []*gauge.StepDiff) []*gauge_messages.TextDiff { 426 textDiffs := []*gauge_messages.TextDiff{} 427 for _, diff := range diffs { 428 newtext := strings.TrimSpace(formatter.FormatStep(diff.NewStep)) 429 if diff.IsConcept && !diff.OldStep.InConcept() { 430 newtext = strings.Replace(newtext, "*", "#", -1) 431 } 432 oldFragments := util.GetLinesFromText(strings.TrimSpace(formatter.FormatStep(&diff.OldStep))) 433 d := &gauge_messages.TextDiff{ 434 Span: &gauge_messages.Span{ 435 Start: int64(diff.OldStep.LineNo), 436 StartChar: 0, 437 End: int64(diff.OldStep.LineNo + len(oldFragments) - 1), 438 EndChar: int64(len(oldFragments[len(oldFragments)-1])), 439 }, 440 Content: newtext, 441 } 442 textDiffs = append(textDiffs, d) 443 } 444 return textDiffs 445 } 446 447 func getFileChanges(specs []*gauge.Specification, conceptDictionary *gauge.ConceptDictionary, specsRefactored map[*gauge.Specification][]*gauge.StepDiff, conceptsRefactored map[string][]*gauge.StepDiff) ([]*gauge_messages.FileChanges, []*gauge_messages.FileChanges) { 448 specFiles := []*gauge_messages.FileChanges{} 449 conceptFiles := []*gauge_messages.FileChanges{} 450 for _, spec := range specs { 451 if stepDiffs, ok := specsRefactored[spec]; ok { 452 formatted := formatter.FormatSpecification(spec) 453 specFiles = append(specFiles, &gauge_messages.FileChanges{FileName: spec.FileName, FileContent: formatted, Diffs: createDiffs(stepDiffs)}) 454 } 455 } 456 conceptMap := formatter.FormatConcepts(conceptDictionary) 457 for file, diffs := range conceptsRefactored { 458 conceptFiles = append(conceptFiles, &gauge_messages.FileChanges{FileName: file, FileContent: conceptMap[file], Diffs: createDiffs(diffs)}) 459 } 460 return specFiles, conceptFiles 461 } 462 463 func printRefactoringSummary(refactoringResult *refactoringResult) { 464 exitCode := 0 465 if !refactoringResult.Success { 466 exitCode = 1 467 for _, err := range refactoringResult.Errors { 468 logger.Errorf(true, "%s \n", err) 469 } 470 } 471 for _, warning := range refactoringResult.Warnings { 472 logger.Warningf(true, "%s \n", warning) 473 } 474 logger.Infof(true, "%d specifications changed.\n", len(refactoringResult.specFilesChanged())) 475 logger.Infof(true, "%d concepts changed.\n", len(refactoringResult.conceptFilesChanged())) 476 logger.Infof(true, "%d files in code changed.\n", len(refactoringResult.RunnerFilesChanged)) 477 os.Exit(exitCode) 478 } 479 480 // RefactorSteps performs rephrase refactoring and prints the refactoring summary which includes errors and warnings generated during refactoring and 481 // files changed during refactoring : specification files, concept files and the implementation files changed. 482 func RefactorSteps(oldStep, newStep string, startChan *runner.StartChannels, specDirs []string) { 483 refactoringResult := PerformRephraseRefactoring(oldStep, newStep, startChan, specDirs) 484 printRefactoringSummary(refactoringResult) 485 }