def test_summarize_compare_results(self): c1 = parse_string(self.cfg_props_imapsync_1) c2 = parse_string(self.cfg_props_imapsync_2) diffs = compare_cfgs(c1, c2) output = StringIO() summarize_cfg_diffs(diffs, output) out = output.getvalue() # Very basic check for now. self.assertRegex(out, r"\[imapsync\]\s*3 keys") self.assertRegex(out, r"\[other2\]")
def test_summarize_compare_results(self): c1 = parse_string(self.cfg_props_imapsync_1) c2 = parse_string(self.cfg_props_imapsync_2) diffs = compare_cfgs(c1, c2) output = StringIO() summarize_cfg_diffs(diffs, output)
def run(self, args): if isinstance(args.target, ConfDirProxy): # If a directory is given instead of a target file, then assume the source filename # and target filename are the same. # Also handle local/default meta: e.g.: ksconf promote local.meta . source_basename = os.path.basename(args.source.name) if source_basename == "local.meta": args.target = args.target.get_file("default.meta") else: args.target = args.target.get_file(source_basename) del source_basename if not os.path.isfile(args.target.name): self.stdout.write( "Target file {} does not exist. Moving source file {} to the target." .format(args.target.name, args.source.name)) # For windows: Close out any open file descriptors first args.target.close() args.source.close() if args.keep: shutil.copy2(args.source.name, args.target.name) else: shutil.move(args.source.name, args.target.name) return # If src/dest are the same, then the file ends up being deleted. Whoops! if _samefile(args.source.name, args.target.name): self.stderr.write( "Aborting. SOURCE and TARGET are the same file!\n") return EXIT_CODE_FAILED_SAFETY_CHECK fp_source = file_fingerprint(args.source.name) fp_target = file_fingerprint(args.target.name) # Todo: Add a safety check prevent accidental merge of unrelated files. # Scenario: promote local/props.conf into default/transforms.conf # Possible check (1) Are basenames are different? (props.conf vs transforms.conf) # Possible check (2) Are there key's in common? (DEST_KEY vs REPORT) # Using #1 for now, consider if there's value in #2 bn_source = os.path.basename(args.source.name) bn_target = os.path.basename(args.target.name) if bn_source.endswith(".meta") and bn_target.endswith(".meta"): # Allow local.meta -> default.meta without --force or a warning message pass elif bn_source != bn_target: # Todo: Allow for interactive prompting when in interactive but not force mode. if args.force: self.stderr.write( "Promoting content across conf file types ({0} --> {1}) because the " "'--force' CLI option was set.\n".format( bn_source, bn_target)) else: self.stderr.write( "Refusing to promote content between different types of configuration " "files. {0} --> {1} If this is intentional, override this safety" "check with '--force'\n".format(bn_source, bn_target)) return EXIT_CODE_FAILED_SAFETY_CHECK # Todo: Preserve comments in the TARGET file. Worry with promoting of comments later... # Parse all config files cfg_src = args.source.data cfg_tgt = args.target.data if not cfg_src: self.stderr.write( "No settings in {}. Nothing to promote.\n".format( args.source.name)) return EXIT_CODE_NOTHING_TO_DO if args.mode == "ask": # Show a summary of how many new stanzas would be copied across; how many key changes. # And either accept all (batch) or pick selectively (batch) delta = compare_cfgs(cfg_tgt, cfg_src, allow_level0=False) delta = [op for op in delta if op.tag != DIFF_OP_DELETE] summarize_cfg_diffs(delta, self.stderr) while True: resp = input("Would you like to apply ALL changes? (y/n/d/q)") resp = resp[:1].lower() if resp == 'q': return EXIT_CODE_USER_QUIT elif resp == 'd': show_diff(self.stdout, delta, headers=(args.source.name, args.target.name)) elif resp == 'y': args.mode = "batch" break elif resp == 'n': args.mode = "interactive" break if args.mode == "interactive": (cfg_final_src, cfg_final_tgt) = self._do_promote_interactive( cfg_src, cfg_tgt, args) else: (cfg_final_src, cfg_final_tgt) = self._do_promote_automatic( cfg_src, cfg_tgt, args) # Minimize race condition: Do file mtime/hash check here. Abort on external change. # Todo: Eventually use temporary files and atomic renames to further minimize the risk # Todo: Make backup '.bak' files (user configurable) # Todo: Avoid rewriting files if NO changes were made. (preserve prior backups) # Todo: Restore file modes and such if file_fingerprint(args.source.name, fp_source): self.stderr.write( "Aborting! External source file changed: {0}\n".format( args.source.name)) return EXIT_CODE_EXTERNAL_FILE_EDIT if file_fingerprint(args.target.name, fp_target): self.stderr.write( "Aborting! External target file changed: {0}\n".format( args.target.name)) return EXIT_CODE_EXTERNAL_FILE_EDIT # Reminder: conf entries are being removed from source and promoted into target args.target.dump(cfg_final_tgt) if not args.keep: # If --keep is set, we never touch the source file. if cfg_final_src: args.source.dump(cfg_final_src) else: # Config file is empty. Should we write an empty file, or remove it? if args.keep_empty: args.source.dump(cfg_final_src) else: args.source.unlink()