mirror of
https://github.com/ansible/awx.git
synced 2026-01-10 15:32:07 -03:30
Create test for using manual & file projects (#15754)
* Create test for using a manual project * Chang default project factory to git, remove project files monkeypatch * skip update of factory project * Initial file scaffolding for feature * Fill in galaxy and names * Add README, describe project folders and dependencies
This commit is contained in:
parent
46403e4312
commit
c43dfde45a
41
awx/main/tests/data/projects/README.md
Normal file
41
awx/main/tests/data/projects/README.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Project data for live tests
|
||||
|
||||
Each folder in this directory is usable as source for a project or role or collection,
|
||||
which is used in tests, particularly the "awx/main/tests/live" tests.
|
||||
|
||||
Although these are not git repositories, test fixtures will make copies,
|
||||
and in the coppied folders, run `git init` type commands, turning them into
|
||||
git repos. This is done in the locations
|
||||
|
||||
- `/var/lib/awx/projects`
|
||||
- `/tmp/live_tests`
|
||||
|
||||
These can then be referenced for manual projects or git via the `file://` protocol.
|
||||
|
||||
## debug
|
||||
|
||||
This is the simplest possible case with 1 playbook with 1 debug task.
|
||||
|
||||
## with_requirements
|
||||
|
||||
This has a playbook that runs a task that uses a role.
|
||||
|
||||
The role project is referenced in the `roles/requirements.yml` file.
|
||||
|
||||
### role_requirement
|
||||
|
||||
This is the source for the role that the `with_requirements` project uses.
|
||||
|
||||
## test_host_query
|
||||
|
||||
This has a playbook that runs a task from a custom collection module which
|
||||
is registered for the host query feature.
|
||||
|
||||
The collection is referenced in its `collections/requirements.yml` file.
|
||||
|
||||
### host_query
|
||||
|
||||
This can act as source code for a collection that enables host/event querying.
|
||||
|
||||
It has a `meta/event_query.yml` file, which may provide you an example of how
|
||||
to implement this in your own collection.
|
||||
6
awx/main/tests/data/projects/debug/debug.yml
Normal file
6
awx/main/tests/data/projects/debug/debug.yml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
- hosts: all
|
||||
gather_facts: false
|
||||
connection: local
|
||||
tasks:
|
||||
- debug: msg='hello'
|
||||
19
awx/main/tests/data/projects/host_query/galaxy.yml
Normal file
19
awx/main/tests/data/projects/host_query/galaxy.yml
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
authors:
|
||||
- AWX Project Contributors <awx-project@googlegroups.com>
|
||||
dependencies: {}
|
||||
description: Indirect host counting example repo. Not for use in production.
|
||||
documentation: https://github.com/ansible/awx
|
||||
homepage: https://github.com/ansible/awx
|
||||
issues: https://github.com/ansible/awx
|
||||
license:
|
||||
- GPL-3.0-or-later
|
||||
name: query
|
||||
namespace: demo
|
||||
readme: README.md
|
||||
repository: https://github.com/ansible/awx
|
||||
tags:
|
||||
- demo
|
||||
- testing
|
||||
- host_counting
|
||||
version: 0.0.1
|
||||
@ -0,0 +1,4 @@
|
||||
---
|
||||
{
|
||||
"demo.query.example": ""
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# Same licensing as AWX
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: example
|
||||
|
||||
short_description: Module for specific live tests
|
||||
|
||||
version_added: "2.0.0"
|
||||
|
||||
description: This module is part of a test collection in local source.
|
||||
|
||||
options:
|
||||
host_name:
|
||||
description: Name to return as the host name.
|
||||
required: false
|
||||
type: str
|
||||
|
||||
author:
|
||||
- AWX Live Tests
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Test with defaults
|
||||
demo.query.example:
|
||||
|
||||
- name: Test with custom host name
|
||||
demo.query.example:
|
||||
host_name: foo_host
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
direct_host_name:
|
||||
description: The name of the host, this will be collected with the feature.
|
||||
type: str
|
||||
returned: always
|
||||
sample: 'foo_host'
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def run_module():
|
||||
module_args = dict(
|
||||
host_name=dict(type='str', required=False, default='foo_host_default'),
|
||||
)
|
||||
|
||||
result = dict(
|
||||
changed=False,
|
||||
other_data='sample_string',
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
result['direct_host_name'] = module.params['host_name']
|
||||
result['nested_host_name'] = {'host_name': module.params['host_name']}
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
def main():
|
||||
run_module()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
19
awx/main/tests/data/projects/role_requirement/meta/main.yml
Normal file
19
awx/main/tests/data/projects/role_requirement/meta/main.yml
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: "For Test"
|
||||
company: AWX
|
||||
license: MIT
|
||||
min_ansible_version: 1.4
|
||||
platforms:
|
||||
- name: EL
|
||||
versions:
|
||||
- 8
|
||||
- 9
|
||||
- name: Fedora
|
||||
versions:
|
||||
- 39
|
||||
- 40
|
||||
- 41
|
||||
categories:
|
||||
- stuff
|
||||
dependencies: []
|
||||
@ -0,0 +1,4 @@
|
||||
---
|
||||
- name: debug variable
|
||||
debug:
|
||||
msg: "1234567890"
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
collections:
|
||||
- name: 'file:///tmp/live_tests/host_query'
|
||||
type: git
|
||||
version: devel
|
||||
@ -0,0 +1,8 @@
|
||||
---
|
||||
- hosts: all
|
||||
gather_facts: false
|
||||
connection: local
|
||||
tasks:
|
||||
- demo.query.example:
|
||||
register: result
|
||||
- debug: var=result
|
||||
@ -0,0 +1,3 @@
|
||||
---
|
||||
- name: role_requirement
|
||||
src: git+file:///tmp/live_tests/role_requirement
|
||||
@ -0,0 +1,7 @@
|
||||
---
|
||||
- hosts: all
|
||||
connection: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- include_role:
|
||||
name: role_requirement
|
||||
@ -99,11 +99,19 @@ def mk_user(name, is_superuser=False, organization=None, team=None, persisted=Tr
|
||||
|
||||
def mk_project(name, organization=None, description=None, persisted=True):
|
||||
description = description or '{}-description'.format(name)
|
||||
project = Project(name=name, description=description, playbook_files=['helloworld.yml', 'alt-helloworld.yml'])
|
||||
project = Project(
|
||||
name=name,
|
||||
description=description,
|
||||
playbook_files=['helloworld.yml', 'alt-helloworld.yml'],
|
||||
scm_type='git',
|
||||
scm_url='https://foo.invalid',
|
||||
scm_revision='1234567890123456789012345678901234567890',
|
||||
scm_update_on_launch=False,
|
||||
)
|
||||
if organization is not None:
|
||||
project.organization = organization
|
||||
if persisted:
|
||||
project.save()
|
||||
project.save(skip_update=True)
|
||||
return project
|
||||
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ class TestUnifiedOrganization:
|
||||
def data_for_model(self, model, orm_style=False):
|
||||
data = {'name': 'foo', 'organization': None}
|
||||
if model == 'JobTemplate':
|
||||
proj = models.Project.objects.create(name="test-proj", playbook_files=['helloworld.yml'])
|
||||
proj = models.Project.objects.create(name="test-proj", playbook_files=['helloworld.yml'], scm_type='git', scm_url='https://foo.invalid')
|
||||
if orm_style:
|
||||
data['project_id'] = proj.id
|
||||
else:
|
||||
|
||||
@ -114,20 +114,6 @@ def team_member(user, team):
|
||||
return ret
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def project_playbooks():
|
||||
"""
|
||||
Return playbook_files as playbooks for manual projects when testing.
|
||||
"""
|
||||
|
||||
class PlaybooksMock(mock.PropertyMock):
|
||||
def __get__(self, obj, obj_type):
|
||||
return obj.playbook_files
|
||||
|
||||
mocked = mock.patch.object(Project, 'playbooks', new_callable=PlaybooksMock)
|
||||
mocked.start()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_computed_fields_right_away(request):
|
||||
def run_me(inventory_id):
|
||||
|
||||
@ -335,7 +335,7 @@ def test_team_project_list(get, team_project_list):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("u,expected_status_code", [('rando', 403), ('org_member', 403), ('org_admin', 201), ('admin', 201)])
|
||||
@pytest.mark.django_db()
|
||||
@pytest.mark.django_db
|
||||
def test_create_project(post, organization, org_admin, org_member, admin, rando, u, expected_status_code):
|
||||
if u == 'rando':
|
||||
u = rando
|
||||
@ -353,11 +353,12 @@ def test_create_project(post, organization, org_admin, org_member, admin, rando,
|
||||
'organization': organization.id,
|
||||
},
|
||||
u,
|
||||
expect=expected_status_code,
|
||||
)
|
||||
print(result.data)
|
||||
assert result.status_code == expected_status_code
|
||||
if expected_status_code == 201:
|
||||
assert Project.objects.filter(name='Project', organization=organization).exists()
|
||||
elif expected_status_code == 403:
|
||||
assert 'do not have permission' in str(result.data['detail'])
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@ -7,7 +7,7 @@ import pytest
|
||||
from awx.main.tests.functional.conftest import * # noqa
|
||||
from awx.main.tests.conftest import load_all_credentials # noqa: F401; pylint: disable=unused-import
|
||||
|
||||
from awx.main.models import Organization
|
||||
from awx.main.models import Organization, Inventory
|
||||
|
||||
|
||||
def wait_to_leave_status(job, status, timeout=30, sleep_time=0.1):
|
||||
@ -25,12 +25,26 @@ def wait_to_leave_status(job, status, timeout=30, sleep_time=0.1):
|
||||
raise RuntimeError(f'Job failed to exit {status} in {timeout} seconds. job_explanation={job.job_explanation} tb={job.result_traceback}')
|
||||
|
||||
|
||||
def wait_for_events(uj, timeout=2):
|
||||
start = time.time()
|
||||
while uj.event_processing_finished is False:
|
||||
time.sleep(0.2)
|
||||
uj.refresh_from_db()
|
||||
if time.time() - start > timeout:
|
||||
break
|
||||
|
||||
|
||||
def unified_job_stdout(uj):
|
||||
wait_for_events(uj)
|
||||
return '\n'.join([event.stdout for event in uj.get_event_queryset().order_by('created')])
|
||||
|
||||
|
||||
def wait_for_job(job, final_status='successful', running_timeout=800):
|
||||
wait_to_leave_status(job, 'pending')
|
||||
wait_to_leave_status(job, 'waiting')
|
||||
wait_to_leave_status(job, 'running', timeout=running_timeout)
|
||||
|
||||
assert job.status == final_status, f'Job was not successful id={job.id} status={job.status}'
|
||||
assert job.status == final_status, f'Job was not successful id={job.id} status={job.status} tb={job.result_traceback} output=\n{unified_job_stdout(job)}'
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@ -39,3 +53,9 @@ def default_org():
|
||||
if org is None:
|
||||
raise Exception('Tests expect Default org to already be created and it is not')
|
||||
return org
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def demo_inv(default_org):
|
||||
inventory, _ = Inventory.objects.get_or_create(name='Demo Inventory', defaults={'organization': default_org})
|
||||
return inventory
|
||||
|
||||
115
awx/main/tests/live/tests/projects/conftest.py
Normal file
115
awx/main/tests/live/tests/projects/conftest.py
Normal file
@ -0,0 +1,115 @@
|
||||
import pytest
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.tests import data
|
||||
from awx.main.models import Project, JobTemplate
|
||||
|
||||
from awx.main.tests.live.tests.conftest import wait_for_job
|
||||
|
||||
PROJ_DATA = os.path.join(os.path.dirname(data.__file__), 'projects')
|
||||
|
||||
|
||||
def _copy_folders(source_path, dest_path, clear=False):
|
||||
"folder-by-folder, copy dirs in the source root dir to the destination root dir"
|
||||
for dirname in os.listdir(source_path):
|
||||
source_dir = os.path.join(source_path, dirname)
|
||||
expected_dir = os.path.join(dest_path, dirname)
|
||||
if clear and os.path.exists(expected_dir):
|
||||
shutil.rmtree(expected_dir)
|
||||
if (not os.path.isdir(source_dir)) or os.path.exists(expected_dir):
|
||||
continue
|
||||
shutil.copytree(source_dir, expected_dir)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def copy_project_folders():
|
||||
proj_root = settings.PROJECTS_ROOT
|
||||
if not os.path.exists(proj_root):
|
||||
os.mkdir(proj_root)
|
||||
_copy_folders(PROJ_DATA, proj_root, clear=True)
|
||||
|
||||
|
||||
GIT_COMMANDS = (
|
||||
'git config --global init.defaultBranch devel; '
|
||||
'git init; '
|
||||
'git config user.email jenkins@ansible.com; '
|
||||
'git config user.name DoneByTest; '
|
||||
'git add .; '
|
||||
'git commit -m "initial commit"'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def live_tmp_folder():
|
||||
path = os.path.join(tempfile.gettempdir(), 'live_tests')
|
||||
if os.path.exists(path):
|
||||
shutil.rmtree(path)
|
||||
os.mkdir(path)
|
||||
_copy_folders(PROJ_DATA, path)
|
||||
for dirname in os.listdir(path):
|
||||
source_dir = os.path.join(path, dirname)
|
||||
subprocess.run(GIT_COMMANDS, cwd=source_dir, shell=True)
|
||||
if path not in settings.AWX_ISOLATION_SHOW_PATHS:
|
||||
settings.AWX_ISOLATION_SHOW_PATHS = settings.AWX_ISOLATION_SHOW_PATHS + [path]
|
||||
return path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_job_from_playbook(default_org, demo_inv, post, admin):
|
||||
def _rf(test_name, playbook, local_path=None, scm_url=None):
|
||||
project_name = f'{test_name} project'
|
||||
jt_name = f'{test_name} JT: {playbook}'
|
||||
|
||||
old_proj = Project.objects.filter(name=project_name).first()
|
||||
if old_proj:
|
||||
old_proj.delete()
|
||||
|
||||
old_jt = JobTemplate.objects.filter(name=jt_name).first()
|
||||
if old_jt:
|
||||
old_jt.delete()
|
||||
|
||||
proj_kwargs = {'name': project_name, 'organization': default_org.id}
|
||||
if local_path:
|
||||
# manual path
|
||||
proj_kwargs['scm_type'] = ''
|
||||
proj_kwargs['local_path'] = local_path
|
||||
elif scm_url:
|
||||
proj_kwargs['scm_type'] = 'git'
|
||||
proj_kwargs['scm_url'] = scm_url
|
||||
else:
|
||||
raise RuntimeError('Need to provide scm_url or local_path')
|
||||
|
||||
result = post(
|
||||
reverse('api:project_list'),
|
||||
proj_kwargs,
|
||||
admin,
|
||||
expect=201,
|
||||
)
|
||||
proj = Project.objects.get(id=result.data['id'])
|
||||
|
||||
if proj.current_job:
|
||||
wait_for_job(proj.current_job)
|
||||
|
||||
assert proj.get_project_path()
|
||||
assert playbook in proj.playbooks
|
||||
|
||||
result = post(
|
||||
reverse('api:job_template_list'),
|
||||
{'name': jt_name, 'project': proj.id, 'playbook': playbook, 'inventory': demo_inv.id},
|
||||
admin,
|
||||
expect=201,
|
||||
)
|
||||
jt = JobTemplate.objects.get(id=result.data['id'])
|
||||
job = jt.create_unified_job()
|
||||
job.signal_start()
|
||||
|
||||
wait_for_job(job)
|
||||
assert job.status == 'successful'
|
||||
|
||||
return _rf
|
||||
2
awx/main/tests/live/tests/projects/test_file_projects.py
Normal file
2
awx/main/tests/live/tests/projects/test_file_projects.py
Normal file
@ -0,0 +1,2 @@
|
||||
def test_git_file_project(live_tmp_folder, run_job_from_playbook):
|
||||
run_job_from_playbook('test_git_file_project', 'debug.yml', scm_url=f'file://{live_tmp_folder}/debug')
|
||||
@ -0,0 +1,3 @@
|
||||
def test_indirect_host_counting(live_tmp_folder, run_job_from_playbook):
|
||||
run_job_from_playbook('test_indirect_host_counting', 'run_task.yml', scm_url=f'file://{live_tmp_folder}/test_host_query')
|
||||
# TODO: add assertions that the host query data is populated
|
||||
@ -0,0 +1,2 @@
|
||||
def test_manual_project(copy_project_folders, run_job_from_playbook):
|
||||
run_job_from_playbook('test_manual_project', 'debug.yml', local_path='debug')
|
||||
@ -5,9 +5,9 @@ import pytest
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from awx.main.tests.live.tests.conftest import wait_for_job
|
||||
from awx.main.tests.live.tests.conftest import wait_for_job, wait_for_events
|
||||
|
||||
from awx.main.models import Project, SystemJobTemplate
|
||||
from awx.main.models import Project, SystemJobTemplate, Job
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@ -54,3 +54,11 @@ def test_cache_is_populated_after_cleanup_job(project_with_requirements):
|
||||
|
||||
# Now, we still have a populated cache
|
||||
assert project_cache_is_populated(project_with_requirements)
|
||||
|
||||
|
||||
def test_git_file_collection_requirement(live_tmp_folder, copy_project_folders, run_job_from_playbook):
|
||||
# this behaves differently, as use_requirements.yml references only the folder, does not include the github name
|
||||
run_job_from_playbook('test_git_file_collection_requirement', 'use_requirement.yml', scm_url=f'file://{live_tmp_folder}/with_requirements')
|
||||
job = Job.objects.filter(name__icontains='test_git_file_collection_requirement').order_by('-created').first()
|
||||
wait_for_events(job)
|
||||
assert '1234567890' in job.job_events.filter(task='debug variable', event='runner_on_ok').first().stdout
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user