text: # vim:ts=4:sw=4:et:
  # Copyright (c) Meta Platforms, Inc. and affiliates.
  #
  # This source code is licensed under the MIT license found in the
  # LICENSE file in the root directory of this source tree.
  
  
  import os
  
  import pywatchman
  from watchman.integration.lib import WatchmanSCMTestCase, WatchmanTestCase
  
  
  def is_ubuntu() -> bool:
      try:
          with open("/etc/lsb-release") as f:
              if "Ubuntu" in f.read():
                  return True
      except Exception:
          pass
      return False
  
  
  @WatchmanTestCase.expand_matrix
  class TestSavedState(WatchmanSCMTestCase.WatchmanSCMTestCase):
      def checkOSApplicability(self) -> None:
          if is_ubuntu():
              self.skipTest("Test is flaky. See Facebook task T36574087.")
          if "CIRCLECI" in os.environ or "TRAVIS" in os.environ:
              self.skipTest("consistently fails on single core machines!")
          if os.name == "nt":
              self.skipTest("The order of events on Windows is funky")
  
      def setUp(self) -> None:
          self.skipIfNoFSMonitor()
          self.root = self.mkdtemp()
          """ Set up a repo with a DAG like this:
  @  changeset:
  |  bookmark:    feature4
  |  tag:         tip
  |  summary:     add f1
  |
  | o  changeset:
  |/   bookmark:    feature2
  |    summary:     remove car
  |
  o  changeset:
  |  bookmark:    main
  |  summary:     add bar and car
  |
  o  changeset:
  |  bookmark:    feature3
  |  summary:     add m2
  |
  | o  changeset:
  |/   bookmark:    feature1
  |    summary:     add m1
  |
  o  changeset:
  |  bookmark:   feature0
  |  summary:    add p1
  |
  o  changeset:
     bookmark:    initial
     summary:     add foo
          """
          self.hg(["init"], cwd=self.root)
          self.touchRelative(self.root, "foo")
          self.hg(["book", "initial"], cwd=self.root)
          self.hg(["addremove"], cwd=self.root)
          self.hg(["commit", "-m", "initial"], cwd=self.root)
          self.hg(["book", "feature0"], cwd=self.root)
          self.touchRelative(self.root, "p1")
          self.hg(["addremove"], cwd=self.root)
          self.hg(["commit", "-m", "add p1"], cwd=self.root)
          self.hg(["book", "feature1"], cwd=self.root)
          self.touchRelative(self.root, "m1")
          self.hg(["addremove"], cwd=self.root)
          self.hg(["commit", "-m", "add m1"], cwd=self.root)
          self.hg(["co", "feature0"], cwd=self.root)
          self.hg(["book", "feature3"], cwd=self.root)
          self.touchRelative(self.root, "m2")
          self.hg(["addremove"], cwd=self.root)
          self.hg(["commit", "-m", "add m2"], cwd=self.root)
          self.hg(["book", "main"], cwd=self.root)
          self.touchRelative(self.root, "bar")
          self.touchRelative(self.root, "car")
          self.hg(["addremove"], cwd=self.root)
          self.hg(["commit", "-m", "add bar and car"], cwd=self.root)
          self.hg(["book", "feature2"], cwd=self.root)
          self.hg(["rm", "car"], cwd=self.root)
          self.hg(["commit", "-m", "remove car"], cwd=self.root)
          self.hg(["co", "main"], cwd=self.root)
          self.hg(["book", "feature4"], cwd=self.root)
          self.touchRelative(self.root, "f1")
          self.hg(["addremove"], cwd=self.root)
          self.hg(["commit", "-m", "add f1"], cwd=self.root)
          self.watchmanCommand("watch", self.root)
  
      def getQuery(self, config):
          return {
              "expression": [
                  "not",
                  ["anyof", ["name", ".hg"], ["match", "hg-check*"], ["dirname", ".hg"]],
              ],
              "fields": ["name"],
              "since": {
                  "scm": {
                      "mergebase-with": "main",
                      "saved-state": {"storage": "local", "config": config},
                  }
              },
          }
  
      def getLocalFilename(self, saved_state_rev, metadata):
          if metadata:
              return saved_state_rev + "_" + metadata
          return saved_state_rev
  
      # Creates a saved state (with no content) for the specified project at the
      # specified bookmark within the specified local storage path.
      def saveState(self, project, bookmark, local_storage, metadata=None):
          saved_state_rev = self.resolveCommitHash(bookmark, cwd=self.root)
          project_dir = os.path.join(local_storage, project)
          if not os.path.isdir(project_dir):
              os.mkdir(project_dir)
          filename = self.getLocalFilename(saved_state_rev, metadata)
          self.touchRelative(project_dir, filename)
          return saved_state_rev
  
      def getConfig(self, result):
          return result["clock"]["scm"]["saved-state"]["config"]
  
      def assertStorageTypeLocal(self, result) -> None:
          self.assertEqual(result["clock"]["scm"]["saved-state"]["storage"], "local")
  
      def assertCommitIDEquals(self, result, commit_id) -> None:
          self.assertEqual(result["clock"]["scm"]["saved-state"]["commit-id"], commit_id)
  
      def assertCommitIDNotPresent(self, result) -> None:
          self.assertTrue("commit-id" not in result["clock"]["scm"]["saved-state"])
  
      def assertSavedStateErrorEquals(self, result, error_string) -> None:
          self.assertEqual(result["saved-state-info"]["error"], error_string)
  
      def assertSavedStateInfo(self, result, path, commit_id) -> None:
          self.assertEqual(
              result["saved-state-info"], {"local-path": path, "commit-id": commit_id}
          )
  
      def assertMergebaseEquals(self, result, mergebase) -> None:
          self.assertEqual(result["clock"]["scm"]["mergebase"], mergebase)
  
      def test_localSavedStateErrorHandling(self) -> None:
          # Local storage should throw if config does not include
          # local-storage-path. Unit tests more extensively test all possible
          # error cases, this just confirms that an example error propagates end
          # to end properly.
          config = {"project": "test"}
          test_query = self.getQuery(config)
          with self.assertRaises(pywatchman.WatchmanError) as ctx:
              self.watchmanCommand("query", self.root, test_query)
          self.assertIn(
              "'local-storage-path' must be present in saved state config",
              str(ctx.exception),
          )
  
      def test_localSavedStateNoStateFound(self) -> None:
          # Local saved state should return no commit id and error message if no
          # valid state found (project does not match states saved above)
          local_storage = self.mkdtemp()
          self.saveState("example_project", "feature3", local_storage)
          self.saveState("example_project", "feature0", local_storage)
          config = {"local-storage-path": local_storage, "project": "does-not-exist"}
          test_query = self.getQuery(config)
          res = self.watchmanCommand("query", self.root, test_query)
          expected_mergebase = self.resolveCommitHash("main", cwd=self.root)
          self.assertMergebaseEquals(res, expected_mergebase)
          self.assertCommitIDNotPresent(res)
          self.assertEqual(self.getConfig(res), config)
          self.assertStorageTypeLocal(res)
          self.assertSavedStateErrorEquals(res, "No suitable saved state found")
          self.assertFileListsEqual(res["files"], ["foo", "p1", "m2", "bar", "car", "f1"])
  
      def test_localSavedStateNotWithinLimit(self) -> None:
          # Local saved state should return an empty commit id, error message,
          # and changed files since prior clock if the first available saved
          # state is not within the limit
          local_storage = self.mkdtemp()
          self.saveState("example_project", "feature3", local_storage)
          self.saveState("example_project", "feature0", local_storage)
          config = {
              "local-storage-path": local_storage,
              "project": "example_project",
              "max-commits": 1,
          }
          test_query = self.getQuery(config)
          res = self.watchmanCommand("query", self.root, test_query)
          self.assertSavedStateErrorEquals(res, "No suitable saved state found")
          self.assertEqual(self.getConfig(res), config)
          self.assertStorageTypeLocal(res)
          expected_mergebase = self.resolveCommitHash("main", cwd=self.root)
          self.assertMergebaseEquals(res, expected_mergebase)
          self.assertFileListsEqual(res["files"], ["foo", "p1", "m2", "bar", "car", "f1"])
  
      def test_localSavedStateNotWithinLimitOmitChangedFiles(self) -> None:
          # Local saved state should return an empty commit id, error message,
          # and changed files since prior clock if the first available saved
          # state is not within the limit
          local_storage = self.mkdtemp()
          self.saveState("example_project", "feature3", local_storage)
          self.saveState("example_project", "feature0", local_storage)
          config = {
              "local-storage-path": local_storage,
              "project": "example_project",
              "max-commits": 1,
          }
          test_query = self.getQuery(config)
          test_query["omit_changed_files"] = True
          res = self.watchmanCommand("query", self.root, test_query)
          self.assertSavedStateErrorEquals(res, "No suitable saved state found")
          self.assertEqual(self.getConfig(res), config)
          self.assertStorageTypeLocal(res)
          expected_mergebase = self.resolveCommitHash("main", cwd=self.root)
          self.assertMergebaseEquals(res, expected_mergebase)
          self.assertFileListsEqual(res["files"], [])
  
      def test_localSavedStateNotWithinLimitError(self) -> None:
          # Local saved state should return an empty commit id, error message,
          # and changed files since prior clock if the first available saved
          # state is not within the limit
          local_storage = self.mkdtemp()
          self.saveState("example_project", "feature3", local_storage)
          self.saveState("example_project", "feature0", local_storage)
          config = {
              "local-storage-path": local_storage,
              "project": "example_project",
              "max-commits": 1,
          }
          test_query = self.getQuery(config)
          test_query["fail_if_no_saved_state"] = True
          with self.assertRaises(pywatchman.WatchmanError) as ctx:
              self.watchmanCommand("query", self.root, test_query)
          self.assertIn(
              (
                  "The merge base changed but no corresponding saved state "
                  "was found for the new merge base"
              ),
              str(ctx.exception),
          )
  
      def test_localSavedStateLookupSuccess(self) -> None:
          # Local saved state should return the saved state commit id, info, and
          # changed files since the saved state if valid state found within limit
          local_storage = self.mkdtemp()
          saved_state_rev_feature3 = self.saveState(
              "example_project", "feature3", local_storage
          )
          self.saveState("example_project", "feature0", local_storage)
          config = {
              "local-storage-path": local_storage,
              "project": "example_project",
              "max-commits": 10,
          }
          test_query = self.getQuery(config)
          res = self.watchmanCommand("query", self.root, test_query)
          expected_mergebase = self.resolveCommitHash("main", cwd=self.root)
          self.assertMergebaseEquals(res, expected_mergebase)
          self.assertStorageTypeLocal(res)
          self.assertCommitIDEquals(res, saved_state_rev_feature3)
          self.assertEqual(self.getConfig(res), config)
          project_dir = os.path.join(local_storage, "example_project")
          expected_path = os.path.join(project_dir, saved_state_rev_feature3)
          self.assertSavedStateInfo(res, expected_path, saved_state_rev_feature3)
          self.assertFileListsEqual(res["files"], ["f1", "bar", "car"])
  
      def test_localSavedStateLookupSuccessOmitChangedFiles(self) -> None:
          # Local saved state should return the saved state commit id, info, and
          # changed files since the saved state if valid state found within limit.
          # Since this sets omit_changed_files, we only return the files from the
          # saved-state to the mergebase
          local_storage = self.mkdtemp()
          saved_state_rev_feature3 = self.saveState(
              "example_project", "feature3", local_storage
          )
          self.saveState("example_project", "feature0", local_storage)
          config = {
              "local-storage-path": local_storage,
              "project": "example_project",
              "max-commits": 10,
          }
          test_query = self.getQuery(config)
          test_query["omit_changed_files"] = True
          res = self.watchmanCommand("query", self.root, test_query)
          expected_mergebase = self.resolveCommitHash("main", cwd=self.root)
          self.assertMergebaseEquals(res, expected_mergebase)
          self.assertStorageTypeLocal(res)
          self.assertCommitIDEquals(res, saved_state_rev_feature3)
          self.assertEqual(self.getConfig(res), config)
          project_dir = os.path.join(local_storage, "example_project")
          expected_path = os.path.join(project_dir, saved_state_rev_feature3)
          self.assertSavedStateInfo(res, expected_path, saved_state_rev_feature3)
          self.assertFileListsEqual(res["files"], [])
  
      def test_localSavedStateLookupSuccessWithMetadata(self) -> None:
          local_storage = self.mkdtemp()
          metadata = "metadata"
          saved_state_rev_feature3 = self.saveState(
              "example_project", "feature3", local_storage, metadata
          )
          self.saveState("example_project", "feature0", local_storage, metadata)
          config = {
              "local-storage-path": local_storage,
              "project": "example_project",
              "project-metadata": metadata,
              "max-commits": 10,
          }
          test_query = self.getQuery(config)
          res = self.watchmanCommand("query", self.root, test_query)
          expected_mergebase = self.resolveCommitHash("main", cwd=self.root)
          self.assertMergebaseEquals(res, expected_mergebase)
          self.assertStorageTypeLocal(res)
          self.assertCommitIDEquals(res, saved_state_rev_feature3)
          self.assertEqual(self.getConfig(res), config)
          project_dir = os.path.join(local_storage, "example_project")
          filepath = self.getLocalFilename(saved_state_rev_feature3, metadata)
          expected_path = os.path.join(project_dir, filepath)
          self.assertSavedStateInfo(res, expected_path, saved_state_rev_feature3)
          self.assertFileListsEqual(res["files"], ["f1", "bar", "car"])
  
      def test_localSavedStateFailureIfMetadataDoesNotMatch(self) -> None:
          local_storage = self.mkdtemp()
          self.saveState("example_project", "feature3", local_storage)
          self.saveState("example_project", "feature0", local_storage)
          config = {
              "local-storage-path": local_storage,
              "project": "example_project",
              "project-metadata": "meta",
              "max-commits": 10,
          }
          test_query = self.getQuery(config)
          res = self.watchmanCommand("query", self.root, test_query)
          self.assertSavedStateErrorEquals(res, "No suitable saved state found")
          self.assertEqual(self.getConfig(res), config)
          self.assertStorageTypeLocal(res)
          expected_mergebase = self.resolveCommitHash("main", cwd=self.root)
          self.assertMergebaseEquals(res, expected_mergebase)
          self.assertFileListsEqual(res["files"], ["foo", "p1", "m2", "bar", "car", "f1"])
  
      def test_localSavedStateFailureIfNoMetadataForFileThatHasIt(self) -> None:
          local_storage = self.mkdtemp()
          metadata = "metadata"
          self.saveState("example_project", "feature3", local_storage, metadata)
          self.saveState("example_project", "feature0", local_storage, metadata)
          config = {
              "local-storage-path": local_storage,
              "project": "example_project",
              "max-commits": 10,
          }
          test_query = self.getQuery(config)
          res = self.watchmanCommand("query", self.root, test_query)
          self.assertSavedStateErrorEquals(res, "No suitable saved state found")
          self.assertEqual(self.getConfig(res), config)
          self.assertStorageTypeLocal(res)
          expected_mergebase = self.resolveCommitHash("main", cwd=self.root)
          self.assertMergebaseEquals(res, expected_mergebase)
          self.assertFileListsEqual(res["files"], ["foo", "p1", "m2", "bar", "car", "f1"])
  
      def test_localSavedStateSubscription(self) -> None:
          local_storage = self.mkdtemp()
          saved_state_rev_feature3 = self.saveState(
              "example_project", "feature3", local_storage
          )
          saved_state_rev_feature0 = self.saveState(
              "example_project", "feature0", local_storage
          )
          # Set up a subscription for the successful query and confirm that the
          # subscription response contains the most recent saved state to the
          # current rev's mergebase, and the changed files since that rev
          config = {
              "local-storage-path": local_storage,
              "project": "example_project",
              "max-commits": 10,
          }
          test_query = self.getQuery(config)
          sub = self.watchmanCommand("subscribe", self.root, "scmsub", test_query)
          self.waitForStatesToVacate(self.root)
          syncTimeout = {"sync_timeout": 1000}
          self.watchmanCommand("flush-subscriptions", self.root, syncTimeout)
          dat = self.getSubFatClocksOnly("scmsub", root=self.root)
          expected_mergebase = self.resolveCommitHash("main", cwd=self.root)
          self.assertMergebaseEquals(sub, expected_mergebase)
          self.assertStorageTypeLocal(sub)
          self.assertEqual(self.getConfig(sub), config)
          self.assertCommitIDEquals(sub, saved_state_rev_feature3)
          project_dir = os.path.join(local_storage, "example_project")
          expected_path = os.path.join(project_dir, saved_state_rev_feature3)
          self.assertSavedStateInfo(sub, expected_path, saved_state_rev_feature3)
          self.assertFileListsEqual(
              self.getConsolidatedFileList(dat), ["f1", "bar", "car"]
          )
          # Check out a rev with the same merge base and confirm saved state
          # commit id is unchanged, info is not present, and file list is updated
          self.hg(["co", "-C", "feature2"], cwd=self.root)
          self.waitForStatesToVacate(self.root)
          self.watchmanCommand("flush-subscriptions", self.root, syncTimeout)
          dat = self.getSubFatClocksOnly("scmsub", root=self.root)
          self.assertFileListsEqual(self.getConsolidatedFileList(dat), ["f1", "car"])
          last_res = dat[-1]
          self.assertTrue("saved-state-info" not in last_res)
          self.assertStorageTypeLocal(last_res)
          self.assertEqual(self.getConfig(last_res), config)
          self.assertMergebaseEquals(last_res, expected_mergebase)
          self.assertCommitIDEquals(last_res, saved_state_rev_feature3)
          # Check out rev with a different mergebase and confirm mergebase
          # changes, new saved state info is returned, and file list is relative
          # to the new mergebase
          self.hg(["co", "-C", "feature1"], cwd=self.root)
          self.waitForStatesToVacate(self.root)
          self.watchmanCommand("flush-subscriptions", self.root, syncTimeout)
          dat = self.getSubFatClocksOnly("scmsub", root=self.root)
          self.assertFileListsEqual(self.getConsolidatedFileList(dat), ["m1"])
          last_res = dat[-1]
          self.assertStorageTypeLocal(last_res)
          self.assertEqual(self.getConfig(last_res), config)
          expected_path = os.path.join(project_dir, saved_state_rev_feature0)
          self.assertSavedStateInfo(last_res, expected_path, saved_state_rev_feature0)
          expected_mergebase = self.resolveCommitHash("feature0", cwd=self.root)
          self.assertMergebaseEquals(last_res, expected_mergebase)
          self.assertCommitIDEquals(last_res, saved_state_rev_feature0)
          # Switch to a commit with saved state for that actual commit and ensure
          # file list is empty
          self.hg(["co", "-C", "feature3"], cwd=self.root)
          self.waitForStatesToVacate(self.root)
          self.watchmanCommand("flush-subscriptions", self.root, syncTimeout)
          dat = self.getSubFatClocksOnly("scmsub", root=self.root)
          self.assertFileListsEqual(self.getConsolidatedFileList(dat), [])
          last_res = dat[-1]
          self.assertStorageTypeLocal(last_res)
          self.assertEqual(self.getConfig(last_res), config)
          expected_path = os.path.join(project_dir, saved_state_rev_feature3)
          self.assertSavedStateInfo(last_res, expected_path, saved_state_rev_feature3)
          expected_mergebase = self.resolveCommitHash("feature3", cwd=self.root)
          self.assertMergebaseEquals(last_res, expected_mergebase)
          self.assertCommitIDEquals(last_res, saved_state_rev_feature3)
          # Make sure that without any saved state available we get a saved state
          # error message, no commit ID is specified, but the subscription
          # otherwise succeeds, and we get all changes since the prior clock
          self.hg(["co", "-C", "initial"], cwd=self.root)
          self.waitForStatesToVacate(self.root)
          self.watchmanCommand("flush-subscriptions", self.root, syncTimeout)
          dat = self.getSubFatClocksOnly("scmsub", root=self.root)
          self.assertFileListsEqual(self.getConsolidatedFileList(dat), ["m2", "p1"])
          last_res = dat[-1]
          self.assertStorageTypeLocal(last_res)
          self.assertEqual(self.getConfig(last_res), config)
          self.assertSavedStateErrorEquals(last_res, "No suitable saved state found")
          expected_mergebase = self.resolveCommitHash("initial", cwd=self.root)
          self.assertMergebaseEquals(last_res, expected_mergebase)
          self.assertCommitIDNotPresent(last_res)
  