Jonathan Maw pushed to branch jonathan/workspace-fragment-create at BuildStream / buildstream
Commits:
- 
59842242
by Jonathan Maw at 2018-11-22T18:00:25Z
- 
e2a41e38
by Jonathan Maw at 2018-11-22T18:00:30Z
- 
cad36035
by Jonathan Maw at 2018-11-22T18:00:30Z
- 
68ad554b
by Jonathan Maw at 2018-11-22T18:00:30Z
- 
ea0dcbc5
by Jonathan Maw at 2018-11-22T18:00:30Z
- 
8b646bad
by Jonathan Maw at 2018-11-22T18:00:30Z
- 
5ba66aeb
by Jonathan Maw at 2018-11-22T18:00:30Z
- 
832ce803
by Jonathan Maw at 2018-11-22T18:00:30Z
9 changed files:
- NEWS
- buildstream/_context.py
- buildstream/_frontend/cli.py
- buildstream/_project.py
- buildstream/_stream.py
- buildstream/_workspaces.py
- buildstream/data/userconfig.yaml
- tests/frontend/workspace.py
- tests/integration/shell.py
Changes:
| ... | ... | @@ -67,6 +67,9 @@ buildstream 1.3.1 | 
| 67 | 67 |      allows the user to set a default location for their creation. This has meant
 | 
| 68 | 68 |      that the new CLI is no longer backwards compatible with buildstream 1.2.
 | 
| 69 | 69 |  | 
| 70 | +  o Opening a workspace now creates a .bstproject.yaml file that allows buildstream
 | |
| 71 | +    commands to be run from a workspace that is not inside a project.
 | |
| 72 | + | |
| 70 | 73 |  | 
| 71 | 74 |  =================
 | 
| 72 | 75 |  buildstream 1.1.5
 | 
| ... | ... | @@ -31,7 +31,7 @@ from ._exceptions import LoadError, LoadErrorReason, BstError | 
| 31 | 31 |  from ._message import Message, MessageType
 | 
| 32 | 32 |  from ._profile import Topics, profile_start, profile_end
 | 
| 33 | 33 |  from ._artifactcache import ArtifactCache
 | 
| 34 | -from ._workspaces import Workspaces
 | |
| 34 | +from ._workspaces import Workspaces, WorkspaceProjectCache
 | |
| 35 | 35 |  from .plugin import _plugin_lookup
 | 
| 36 | 36 |  | 
| 37 | 37 |  | 
| ... | ... | @@ -121,6 +121,10 @@ class Context(): | 
| 121 | 121 |          # remove a workspace directory.
 | 
| 122 | 122 |          self.prompt_workspace_close_remove_dir = None
 | 
| 123 | 123 |  | 
| 124 | +        # Boolean, whether we double-check with the user that they meant to
 | |
| 125 | +        # close the workspace when they're using it to access the project.
 | |
| 126 | +        self.prompt_workspace_close_project_inaccessible = None
 | |
| 127 | + | |
| 124 | 128 |          # Boolean, whether we double-check with the user that they meant to do
 | 
| 125 | 129 |          # a hard reset of a workspace, potentially losing changes.
 | 
| 126 | 130 |          self.prompt_workspace_reset_hard = None
 | 
| ... | ... | @@ -139,6 +143,7 @@ class Context(): | 
| 139 | 143 |          self._projects = []
 | 
| 140 | 144 |          self._project_overrides = {}
 | 
| 141 | 145 |          self._workspaces = None
 | 
| 146 | +        self._workspace_project_cache = WorkspaceProjectCache()
 | |
| 142 | 147 |          self._log_handle = None
 | 
| 143 | 148 |          self._log_filename = None
 | 
| 144 | 149 |  | 
| ... | ... | @@ -248,12 +253,15 @@ class Context(): | 
| 248 | 253 |              defaults, Mapping, 'prompt')
 | 
| 249 | 254 |          _yaml.node_validate(prompt, [
 | 
| 250 | 255 |              'auto-init', 'really-workspace-close-remove-dir',
 | 
| 256 | +            'really-workspace-close-project-inaccessible',
 | |
| 251 | 257 |              'really-workspace-reset-hard',
 | 
| 252 | 258 |          ])
 | 
| 253 | 259 |          self.prompt_auto_init = _node_get_option_str(
 | 
| 254 | 260 |              prompt, 'auto-init', ['ask', 'no']) == 'ask'
 | 
| 255 | 261 |          self.prompt_workspace_close_remove_dir = _node_get_option_str(
 | 
| 256 | 262 |              prompt, 'really-workspace-close-remove-dir', ['ask', 'yes']) == 'ask'
 | 
| 263 | +        self.prompt_workspace_close_project_inaccessible = _node_get_option_str(
 | |
| 264 | +            prompt, 'really-workspace-close-project-inaccessible', ['ask', 'yes']) == 'ask'
 | |
| 257 | 265 |          self.prompt_workspace_reset_hard = _node_get_option_str(
 | 
| 258 | 266 |              prompt, 'really-workspace-reset-hard', ['ask', 'yes']) == 'ask'
 | 
| 259 | 267 |  | 
| ... | ... | @@ -310,6 +318,16 @@ class Context(): | 
| 310 | 318 |      def get_workspaces(self):
 | 
| 311 | 319 |          return self._workspaces
 | 
| 312 | 320 |  | 
| 321 | +    # get_workspace_project_cache():
 | |
| 322 | +    #
 | |
| 323 | +    # Return the WorkspaceProjectCache object used for this BuildStream invocation
 | |
| 324 | +    #
 | |
| 325 | +    # Returns:
 | |
| 326 | +    #    (WorkspaceProjectCache): The WorkspaceProjectCache object
 | |
| 327 | +    #
 | |
| 328 | +    def get_workspace_project_cache(self):
 | |
| 329 | +        return self._workspace_project_cache
 | |
| 330 | + | |
| 313 | 331 |      # get_overrides():
 | 
| 314 | 332 |      #
 | 
| 315 | 333 |      # Fetch the override dictionary for the active project. This returns
 | 
| ... | ... | @@ -747,11 +747,18 @@ def workspace_close(app, remove_dir, all_, elements): | 
| 747 | 747 |  | 
| 748 | 748 |          elements = app.stream.redirect_element_names(elements)
 | 
| 749 | 749 |  | 
| 750 | -        # Check that the workspaces in question exist
 | |
| 750 | +        # Check that the workspaces in question exist, and that it's safe to
 | |
| 751 | +        # remove them.
 | |
| 751 | 752 |          nonexisting = []
 | 
| 752 | 753 |          for element_name in elements:
 | 
| 753 | 754 |              if not app.stream.workspace_exists(element_name):
 | 
| 754 | 755 |                  nonexisting.append(element_name)
 | 
| 756 | +            if (app.stream.workspace_is_required(element_name) and app.interactive and
 | |
| 757 | +                    app.context.prompt_workspace_close_project_inaccessible):
 | |
| 758 | +                click.echo("Removing '{}' will prevent you from running buildstream commands".format(element_name))
 | |
| 759 | +                if not click.confirm('Are you sure you want to close this workspace?'):
 | |
| 760 | +                    click.echo('Aborting', err=True)
 | |
| 761 | +                    sys.exit(-1)
 | |
| 755 | 762 |          if nonexisting:
 | 
| 756 | 763 |              raise AppError("Workspace does not exist", detail="\n".join(nonexisting))
 | 
| 757 | 764 |  | 
| ... | ... | @@ -94,8 +94,10 @@ class Project(): | 
| 94 | 94 |          # The project name
 | 
| 95 | 95 |          self.name = None
 | 
| 96 | 96 |  | 
| 97 | -        # The project directory
 | |
| 98 | -        self.directory = self._find_project_dir(directory)
 | |
| 97 | +        self._context = context  # The invocation Context, a private member
 | |
| 98 | + | |
| 99 | +        # The project directory, and whether the project was found from an external workspace
 | |
| 100 | +        self.directory, self._required_workspace_element = self._find_project_dir(directory)
 | |
| 99 | 101 |  | 
| 100 | 102 |          # Absolute path to where elements are loaded from within the project
 | 
| 101 | 103 |          self.element_path = None
 | 
| ... | ... | @@ -116,7 +118,6 @@ class Project(): | 
| 116 | 118 |          #
 | 
| 117 | 119 |          # Private Members
 | 
| 118 | 120 |          #
 | 
| 119 | -        self._context = context  # The invocation Context
 | |
| 120 | 121 |  | 
| 121 | 122 |          self._default_mirror = default_mirror    # The name of the preferred mirror.
 | 
| 122 | 123 |  | 
| ... | ... | @@ -370,6 +371,14 @@ class Project(): | 
| 370 | 371 |  | 
| 371 | 372 |          self._load_second_pass()
 | 
| 372 | 373 |  | 
| 374 | +    # required_workspace_element()
 | |
| 375 | +    #
 | |
| 376 | +    # Returns the element whose workspace is required to load this project,
 | |
| 377 | +    # if any.
 | |
| 378 | +    #
 | |
| 379 | +    def required_workspace_element(self):
 | |
| 380 | +        return self._required_workspace_element
 | |
| 381 | + | |
| 373 | 382 |      # cleanup()
 | 
| 374 | 383 |      #
 | 
| 375 | 384 |      # Cleans up resources used loading elements
 | 
| ... | ... | @@ -662,18 +671,26 @@ class Project(): | 
| 662 | 671 |      # Raises:
 | 
| 663 | 672 |      #    LoadError if project.conf is not found
 | 
| 664 | 673 |      #
 | 
| 674 | +    # Returns:
 | |
| 675 | +    #    (str) - the directory that contains the project, and
 | |
| 676 | +    #    (str) - the name of the element required to find the project, or an empty string
 | |
| 677 | +    #
 | |
| 665 | 678 |      def _find_project_dir(self, directory):
 | 
| 666 | -        directory = os.path.abspath(directory)
 | |
| 667 | -        while not os.path.isfile(os.path.join(directory, _PROJECT_CONF_FILE)):
 | |
| 668 | -            parent_dir = os.path.dirname(directory)
 | |
| 669 | -            if directory == parent_dir:
 | |
| 679 | +        workspace_element = ""
 | |
| 680 | +        project_directory = utils._search_upward_for_file(directory, _PROJECT_CONF_FILE)
 | |
| 681 | +        if not project_directory:
 | |
| 682 | +            workspace_project_cache = self._context.get_workspace_project_cache()
 | |
| 683 | +            workspace_project = workspace_project_cache.get(directory)
 | |
| 684 | +            if workspace_project.has_projects():
 | |
| 685 | +                project_directory = workspace_project.get_default_path()
 | |
| 686 | +                workspace_element = workspace_project.get_default_element()
 | |
| 687 | +            else:
 | |
| 670 | 688 |                  raise LoadError(
 | 
| 671 | 689 |                      LoadErrorReason.MISSING_PROJECT_CONF,
 | 
| 672 | 690 |                      '{} not found in current directory or any of its parent directories'
 | 
| 673 | 691 |                      .format(_PROJECT_CONF_FILE))
 | 
| 674 | -            directory = parent_dir
 | |
| 675 | 692 |  | 
| 676 | -        return directory
 | |
| 693 | +        return project_directory, workspace_element
 | |
| 677 | 694 |  | 
| 678 | 695 |      def _load_plugin_factories(self, config, output):
 | 
| 679 | 696 |          plugin_source_origins = []   # Origins of custom sources
 | 
| ... | ... | @@ -550,6 +550,8 @@ class Stream(): | 
| 550 | 550 |          # So far this function has tried to catch as many issues as possible with out making any changes
 | 
| 551 | 551 |          # Now it dose the bits that can not be made atomic.
 | 
| 552 | 552 |          targetGenerator = zip(elements, expanded_directories)
 | 
| 553 | +        workspace_project_cache = self._context.get_workspace_project_cache()
 | |
| 554 | +        project = self._context.get_toplevel_project()
 | |
| 553 | 555 |          for target, directory in targetGenerator:
 | 
| 554 | 556 |              self._message(MessageType.INFO, "Creating workspace for element {}"
 | 
| 555 | 557 |                            .format(target.name))
 | 
| ... | ... | @@ -574,6 +576,10 @@ class Stream(): | 
| 574 | 576 |                  with target.timed_activity("Staging sources to {}".format(directory)):
 | 
| 575 | 577 |                      target._open_workspace()
 | 
| 576 | 578 |  | 
| 579 | +            workspace_project = workspace_project_cache.add(directory, project.directory,
 | |
| 580 | +                                                            target._get_full_name())
 | |
| 581 | +            workspace_project.write()
 | |
| 582 | + | |
| 577 | 583 |              # Saving the workspace once it is set up means that if the next workspace fails to be created before
 | 
| 578 | 584 |              # the configuration gets saved. The successfully created workspace still gets saved.
 | 
| 579 | 585 |              workspaces.save_config()
 | 
| ... | ... | @@ -601,6 +607,9 @@ class Stream(): | 
| 601 | 607 |                  except OSError as e:
 | 
| 602 | 608 |                      raise StreamError("Could not remove  '{}': {}"
 | 
| 603 | 609 |                                        .format(workspace.get_absolute_path(), e)) from e
 | 
| 610 | +        else:
 | |
| 611 | +            workspace_project_cache = self._context.get_workspace_project_cache()
 | |
| 612 | +            workspace_project_cache.remove(workspace.get_absolute_path())
 | |
| 604 | 613 |  | 
| 605 | 614 |          # Delete the workspace and save the configuration
 | 
| 606 | 615 |          workspaces.delete_workspace(element_name)
 | 
| ... | ... | @@ -644,6 +653,8 @@ class Stream(): | 
| 644 | 653 |          for element in elements:
 | 
| 645 | 654 |              workspace = workspaces.get_workspace(element._get_full_name())
 | 
| 646 | 655 |              workspace_path = workspace.get_absolute_path()
 | 
| 656 | +            workspace_project_cache = self._context.get_workspace_project_cache()
 | |
| 657 | +            workspace_project = workspace_project_cache.get(workspace_path)
 | |
| 647 | 658 |              if soft:
 | 
| 648 | 659 |                  workspace.prepared = False
 | 
| 649 | 660 |                  self._message(MessageType.INFO, "Reset workspace state for {} at: {}"
 | 
| ... | ... | @@ -664,6 +675,8 @@ class Stream(): | 
| 664 | 675 |              with element.timed_activity("Staging sources to {}".format(workspace_path)):
 | 
| 665 | 676 |                  element._open_workspace()
 | 
| 666 | 677 |  | 
| 678 | +            workspace_project.write()
 | |
| 679 | + | |
| 667 | 680 |              self._message(MessageType.INFO,
 | 
| 668 | 681 |                            "Reset workspace for {} at: {}".format(element.name,
 | 
| 669 | 682 |                                                                   workspace_path))
 | 
| ... | ... | @@ -694,6 +707,20 @@ class Stream(): | 
| 694 | 707 |  | 
| 695 | 708 |          return False
 | 
| 696 | 709 |  | 
| 710 | +    # workspace_is_required()
 | |
| 711 | +    #
 | |
| 712 | +    # Checks whether the workspace belonging to element_name is required to
 | |
| 713 | +    # load the project
 | |
| 714 | +    #
 | |
| 715 | +    # Args:
 | |
| 716 | +    #    element_name (str): The element whose workspace may be required
 | |
| 717 | +    #
 | |
| 718 | +    # Returns:
 | |
| 719 | +    #    (bool): True if the workspace is required
 | |
| 720 | +    def workspace_is_required(self, element_name):
 | |
| 721 | +        required_elm = self._project.required_workspace_element()
 | |
| 722 | +        return required_elm == element_name
 | |
| 723 | + | |
| 697 | 724 |      # workspace_list
 | 
| 698 | 725 |      #
 | 
| 699 | 726 |      # Serializes the workspaces and dumps them in YAML to stdout.
 | 
| ... | ... | @@ -377,10 +377,15 @@ class Workspace(): | 
| 377 | 377 |          if recalculate or self._key is None:
 | 
| 378 | 378 |              fullpath = self.get_absolute_path()
 | 
| 379 | 379 |  | 
| 380 | +            excluded_files = (WORKSPACE_PROJECT_FILE,)
 | |
| 381 | + | |
| 380 | 382 |              # Get a list of tuples of the the project relative paths and fullpaths
 | 
| 381 | 383 |              if os.path.isdir(fullpath):
 | 
| 382 | 384 |                  filelist = utils.list_relative_paths(fullpath)
 | 
| 383 | -                filelist = [(relpath, os.path.join(fullpath, relpath)) for relpath in filelist]
 | |
| 385 | +                filelist = [
 | |
| 386 | +                    (relpath, os.path.join(fullpath, relpath)) for relpath in filelist
 | |
| 387 | +                    if relpath not in excluded_files
 | |
| 388 | +                ]
 | |
| 384 | 389 |              else:
 | 
| 385 | 390 |                  filelist = [(self.get_absolute_path(), fullpath)]
 | 
| 386 | 391 |  | 
| ... | ... | @@ -128,6 +128,14 @@ prompt: | 
| 128 | 128 |    #
 | 
| 129 | 129 |    really-workspace-close-remove-dir: ask
 | 
| 130 | 130 |  | 
| 131 | +  # Whether to really proceed with 'bst workspace close' when doing so would
 | |
| 132 | +  # stop them from running bst commands in this workspace.
 | |
| 133 | +  #
 | |
| 134 | +  #  ask - Ask the user if they are sure.
 | |
| 135 | +  #  yes - Always close, without asking.
 | |
| 136 | +  #
 | |
| 137 | +  really-workspace-close-project-inaccessible: ask
 | |
| 138 | + | |
| 131 | 139 |    # Whether to really proceed with 'bst workspace reset' doing a hard reset of
 | 
| 132 | 140 |    # a workspace, potentially losing changes.
 | 
| 133 | 141 |    #
 | 
| ... | ... | @@ -31,6 +31,7 @@ import shutil | 
| 31 | 31 |  import subprocess
 | 
| 32 | 32 |  from ruamel.yaml.comments import CommentedSet
 | 
| 33 | 33 |  from tests.testutils import cli, create_repo, ALL_REPO_KINDS, wait_for_cache_granularity
 | 
| 34 | +from tests.testutils import create_artifact_share
 | |
| 34 | 35 |  | 
| 35 | 36 |  from buildstream import _yaml
 | 
| 36 | 37 |  from buildstream._exceptions import ErrorDomain, LoadError, LoadErrorReason
 | 
| ... | ... | @@ -615,9 +616,12 @@ def test_list(cli, tmpdir, datafiles): | 
| 615 | 616 |  @pytest.mark.datafiles(DATA_DIR)
 | 
| 616 | 617 |  @pytest.mark.parametrize("kind", repo_kinds)
 | 
| 617 | 618 |  @pytest.mark.parametrize("strict", [("strict"), ("non-strict")])
 | 
| 618 | -def test_build(cli, tmpdir, datafiles, kind, strict):
 | |
| 619 | +@pytest.mark.parametrize("call_from", [("project"), ("workspace")])
 | |
| 620 | +def test_build(cli, tmpdir_factory, datafiles, kind, strict, call_from):
 | |
| 621 | +    tmpdir = tmpdir_factory.mktemp('')
 | |
| 619 | 622 |      element_name, project, workspace = open_workspace(cli, tmpdir, datafiles, kind, False)
 | 
| 620 | 623 |      checkout = os.path.join(str(tmpdir), 'checkout')
 | 
| 624 | +    args_pre = ['-C', workspace] if call_from == "project" else []
 | |
| 621 | 625 |  | 
| 622 | 626 |      # Modify workspace
 | 
| 623 | 627 |      shutil.rmtree(os.path.join(workspace, 'usr', 'bin'))
 | 
| ... | ... | @@ -640,15 +644,14 @@ def test_build(cli, tmpdir, datafiles, kind, strict): | 
| 640 | 644 |      # Build modified workspace
 | 
| 641 | 645 |      assert cli.get_element_state(project, element_name) == 'buildable'
 | 
| 642 | 646 |      assert cli.get_element_key(project, element_name) == "{:?<64}".format('')
 | 
| 643 | -    result = cli.run(project=project, args=['build', element_name])
 | |
| 647 | +    result = cli.run(project=project, args=args_pre + ['build', element_name])
 | |
| 644 | 648 |      result.assert_success()
 | 
| 645 | 649 |      assert cli.get_element_state(project, element_name) == 'cached'
 | 
| 646 | 650 |      assert cli.get_element_key(project, element_name) != "{:?<64}".format('')
 | 
| 647 | 651 |  | 
| 648 | 652 |      # Checkout the result
 | 
| 649 | -    result = cli.run(project=project, args=[
 | |
| 650 | -        'checkout', element_name, checkout
 | |
| 651 | -    ])
 | |
| 653 | +    result = cli.run(project=project,
 | |
| 654 | +                     args=args_pre + ['checkout', element_name, checkout])
 | |
| 652 | 655 |      result.assert_success()
 | 
| 653 | 656 |  | 
| 654 | 657 |      # Check that the pony.conf from the modified workspace exists
 | 
| ... | ... | @@ -1055,3 +1058,131 @@ def test_multiple_failed_builds(cli, tmpdir, datafiles): | 
| 1055 | 1058 |          result = cli.run(project=project, args=["build", element_name])
 | 
| 1056 | 1059 |          assert "BUG" not in result.stderr
 | 
| 1057 | 1060 |          assert cli.get_element_state(project, element_name) != "cached"
 | 
| 1061 | + | |
| 1062 | + | |
| 1063 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 1064 | +def test_external_fetch(cli, datafiles, tmpdir_factory):
 | |
| 1065 | +    # Fetching from a workspace outside a project doesn't fail horribly
 | |
| 1066 | +    tmpdir = tmpdir_factory.mktemp('')
 | |
| 1067 | +    element_name, project, workspace = open_workspace(cli, tmpdir, datafiles, "git", False)
 | |
| 1068 | + | |
| 1069 | +    result = cli.run(project=project, args=['-C', workspace, 'fetch', element_name])
 | |
| 1070 | +    result.assert_success()
 | |
| 1071 | + | |
| 1072 | +    # We already fetched it by opening the workspace, but we're also checking
 | |
| 1073 | +    # `bst show` works here
 | |
| 1074 | +    assert cli.get_element_state(project, element_name) == 'buildable'
 | |
| 1075 | + | |
| 1076 | + | |
| 1077 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 1078 | +def test_external_push_pull(cli, datafiles, tmpdir_factory):
 | |
| 1079 | +    # Pushing and pulling to/from an artifact cache works from an external workspace
 | |
| 1080 | +    tmpdir = tmpdir_factory.mktemp('')
 | |
| 1081 | +    element_name, project, workspace = open_workspace(cli, tmpdir, datafiles, "git", False)
 | |
| 1082 | + | |
| 1083 | +    with create_artifact_share(os.path.join(str(tmpdir), 'artifactshare')) as share:
 | |
| 1084 | +        result = cli.run(project=project, args=['-C', workspace, 'build', element_name])
 | |
| 1085 | +        result.assert_success()
 | |
| 1086 | + | |
| 1087 | +        cli.configure({
 | |
| 1088 | +            'artifacts': {'url': share.repo, 'push': True}
 | |
| 1089 | +        })
 | |
| 1090 | + | |
| 1091 | +        result = cli.run(project=project, args=['-C', workspace, 'push', element_name])
 | |
| 1092 | +        result.assert_success()
 | |
| 1093 | + | |
| 1094 | +        result = cli.run(project=project, args=['-C', workspace, 'pull', '--deps', 'all', 'target.bst'])
 | |
| 1095 | +        result.assert_success()
 | |
| 1096 | + | |
| 1097 | + | |
| 1098 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 1099 | +def test_external_track(cli, datafiles, tmpdir_factory):
 | |
| 1100 | +    # Tracking does not get horribly confused
 | |
| 1101 | +    tmpdir = tmpdir_factory.mktemp('')
 | |
| 1102 | +    element_name, project, workspace = open_workspace(cli, tmpdir, datafiles, "git", True)
 | |
| 1103 | + | |
| 1104 | +    # The workspace is necessarily already tracked, so we only care that
 | |
| 1105 | +    # there's no weird errors.
 | |
| 1106 | +    result = cli.run(project=project, args=['-C', workspace, 'track', element_name])
 | |
| 1107 | +    result.assert_success()
 | |
| 1108 | + | |
| 1109 | + | |
| 1110 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 1111 | +def test_external_open_other(cli, datafiles, tmpdir_factory):
 | |
| 1112 | +    # >From inside an external workspace, open another workspace
 | |
| 1113 | +    tmpdir1 = tmpdir_factory.mktemp('')
 | |
| 1114 | +    tmpdir2 = tmpdir_factory.mktemp('')
 | |
| 1115 | +    # Making use of the assumption that it's the same project in both invocations of open_workspace
 | |
| 1116 | +    alpha_element, project, alpha_workspace = open_workspace(cli, tmpdir1, datafiles, "git", False, suffix="-alpha")
 | |
| 1117 | +    beta_element, _, beta_workspace = open_workspace(cli, tmpdir2, datafiles, "git", False, suffix="-beta")
 | |
| 1118 | + | |
| 1119 | +    # Closing the other element first, because I'm too lazy to create an
 | |
| 1120 | +    # element without opening it
 | |
| 1121 | +    result = cli.run(project=project, args=['workspace', 'close', beta_element])
 | |
| 1122 | +    result.assert_success()
 | |
| 1123 | + | |
| 1124 | +    result = cli.run(project=project, args=[
 | |
| 1125 | +        '-C', alpha_workspace, 'workspace', 'open', '--force', '--directory', beta_workspace, beta_element
 | |
| 1126 | +    ])
 | |
| 1127 | +    result.assert_success()
 | |
| 1128 | + | |
| 1129 | + | |
| 1130 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 1131 | +def test_external_close_other(cli, datafiles, tmpdir_factory):
 | |
| 1132 | +    # >From inside an external workspace, close the other workspace
 | |
| 1133 | +    tmpdir1 = tmpdir_factory.mktemp('')
 | |
| 1134 | +    tmpdir2 = tmpdir_factory.mktemp('')
 | |
| 1135 | +    # Making use of the assumption that it's the same project in both invocations of open_workspace
 | |
| 1136 | +    alpha_element, project, alpha_workspace = open_workspace(cli, tmpdir1, datafiles, "git", False, suffix="-alpha")
 | |
| 1137 | +    beta_element, _, beta_workspace = open_workspace(cli, tmpdir2, datafiles, "git", False, suffix="-beta")
 | |
| 1138 | + | |
| 1139 | +    result = cli.run(project=project, args=['-C', alpha_workspace, 'workspace', 'close', beta_element])
 | |
| 1140 | +    result.assert_success()
 | |
| 1141 | + | |
| 1142 | + | |
| 1143 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 1144 | +def test_external_close_self(cli, datafiles, tmpdir_factory):
 | |
| 1145 | +    # >From inside an external workspace, close it
 | |
| 1146 | +    tmpdir1 = tmpdir_factory.mktemp('')
 | |
| 1147 | +    tmpdir2 = tmpdir_factory.mktemp('')
 | |
| 1148 | +    # Making use of the assumption that it's the same project in both invocations of open_workspace
 | |
| 1149 | +    alpha_element, project, alpha_workspace = open_workspace(cli, tmpdir1, datafiles, "git", False, suffix="-alpha")
 | |
| 1150 | +    beta_element, _, beta_workspace = open_workspace(cli, tmpdir2, datafiles, "git", False, suffix="-beta")
 | |
| 1151 | + | |
| 1152 | +    result = cli.run(project=project, args=['-C', alpha_workspace, 'workspace', 'close', alpha_element])
 | |
| 1153 | +    result.assert_success()
 | |
| 1154 | + | |
| 1155 | + | |
| 1156 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 1157 | +def test_external_reset_other(cli, datafiles, tmpdir_factory):
 | |
| 1158 | +    tmpdir1 = tmpdir_factory.mktemp('')
 | |
| 1159 | +    tmpdir2 = tmpdir_factory.mktemp('')
 | |
| 1160 | +    # Making use of the assumption that it's the same project in both invocations of open_workspace
 | |
| 1161 | +    alpha_element, project, alpha_workspace = open_workspace(cli, tmpdir1, datafiles, "git", False, suffix="-alpha")
 | |
| 1162 | +    beta_element, _, beta_workspace = open_workspace(cli, tmpdir2, datafiles, "git", False, suffix="-beta")
 | |
| 1163 | + | |
| 1164 | +    result = cli.run(project=project, args=['-C', alpha_workspace, 'workspace', 'reset', beta_element])
 | |
| 1165 | +    result.assert_success()
 | |
| 1166 | + | |
| 1167 | + | |
| 1168 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 1169 | +def test_external_reset_self(cli, datafiles, tmpdir):
 | |
| 1170 | +    element, project, workspace = open_workspace(cli, tmpdir, datafiles, "git", False)
 | |
| 1171 | + | |
| 1172 | +    # Command succeeds
 | |
| 1173 | +    result = cli.run(project=project, args=['-C', workspace, 'workspace', 'reset', element])
 | |
| 1174 | +    result.assert_success()
 | |
| 1175 | + | |
| 1176 | +    # Successive commands still work (i.e. .bstproject.yaml hasn't been deleted)
 | |
| 1177 | +    result = cli.run(project=project, args=['-C', workspace, 'workspace', 'list'])
 | |
| 1178 | +    result.assert_success()
 | |
| 1179 | + | |
| 1180 | + | |
| 1181 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 1182 | +def test_external_list(cli, datafiles, tmpdir_factory):
 | |
| 1183 | +    tmpdir = tmpdir_factory.mktemp('')
 | |
| 1184 | +    # Making use of the assumption that it's the same project in both invocations of open_workspace
 | |
| 1185 | +    element, project, workspace = open_workspace(cli, tmpdir, datafiles, "git", False)
 | |
| 1186 | + | |
| 1187 | +    result = cli.run(project=project, args=['-C', workspace, 'workspace', 'list'])
 | |
| 1188 | +    result.assert_success() | 
| ... | ... | @@ -353,3 +353,29 @@ def test_integration_devices(cli, tmpdir, datafiles): | 
| 353 | 353 |  | 
| 354 | 354 |      result = execute_shell(cli, project, ["true"], element=element_name)
 | 
| 355 | 355 |      assert result.exit_code == 0
 | 
| 356 | + | |
| 357 | + | |
| 358 | +# Test that a shell can be opened from an external workspace
 | |
| 359 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 360 | +@pytest.mark.parametrize("build_shell", [("build"), ("nobuild")])
 | |
| 361 | +@pytest.mark.skipif(IS_LINUX and not HAVE_BWRAP, reason='Only available with bubblewrap on Linux')
 | |
| 362 | +def test_integration_external_workspace(cli, tmpdir_factory, datafiles, build_shell):
 | |
| 363 | +    tmpdir = tmpdir_factory.mktemp("")
 | |
| 364 | +    project = os.path.join(datafiles.dirname, datafiles.basename)
 | |
| 365 | +    element_name = 'autotools/amhello.bst'
 | |
| 366 | +    workspace_dir = os.path.join(str(tmpdir), 'workspace')
 | |
| 367 | + | |
| 368 | +    result = cli.run(project=project, args=[
 | |
| 369 | +        'workspace', 'open', '--directory', workspace_dir, element_name
 | |
| 370 | +    ])
 | |
| 371 | +    result.assert_success()
 | |
| 372 | + | |
| 373 | +    result = cli.run(project=project, args=['-C', workspace_dir, 'build', element_name])
 | |
| 374 | +    result.assert_success()
 | |
| 375 | + | |
| 376 | +    command = ['shell']
 | |
| 377 | +    if build_shell == 'build':
 | |
| 378 | +        command.append('--build')
 | |
| 379 | +    command.extend([element_name, '--', 'true'])
 | |
| 380 | +    result = cli.run(project=project, cwd=workspace_dir, args=command)
 | |
| 381 | +    result.assert_success() | 
