petevg: $$merge$$
petevg: $$merge$$
wallyworld: $$merge$$
johnsca: I cherry-picked #418 into this hoping to get a full passing run.  Uncovered at least #420 and #421 
johnsca: !!build!!
johnsca: $$merge$$
diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml
new file mode 100644
index 0000000..2a48ae8
--- /dev/null
+++ b/.github/workflows/tox.yaml
@@ -0,0 +1,40 @@
+name: Run tests with Tox
+
+on: [push]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python: [3.6, 3.7, 3.8]
+        juju: [stable, edge]
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Setup Python
+        uses: actions/setup-python@v1
+        with:
+          python-version: ${{ matrix.python }}
+      - name: Install Tox and any other packages
+        run: pip install tox
+      - name: Run Lint and Unit Tests
+        run: tox -e lint,py3
+      - name: Install Snaps
+        run: |
+          sudo snap install lxd
+          sudo snap install juju --classic --channel ${{ matrix.juju }}
+          sudo snap install juju-wait --classic
+      - name: Configure LXD
+        run: |
+          sudo lxd waitready --timeout 30
+          sudo chmod 666 /var/snap/lxd/common/lxd/unix.socket
+          lxd init --auto --network-address='[::]' --network-port=8443 --storage-backend=dir
+          lxc network set lxdbr0 ipv6.address none
+      - name: Bootstrap Juju Controller
+        run: juju bootstrap localhost test --config 'identity-url=https://api.staging.jujucharms.com/identity' --config 'allow-model-access=true' --config 'test-mode=true'
+      - name: Run Integration Tests
+        run: tox -e integration,serial
+        env:
+          TEST_AGENTS: '{"agents":[{"url":"https://api.staging.jujucharms.com/identity","username":"libjuju-ci@yellow"}],"key":{"private":"88OOCxIHQNguRG7zFg2y2Hx5Ob0SeVKKBRnjyehverc=","public":"fDn20+5FGyN2hYO7z0rFUyoHGUnfrleslUNtoYsjNSs="}}'
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 50ba3e2..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-dist: xenial
-sudo: required
-language: python
-cache: pip
-before_install:
-  - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then sudo add-apt-repository -y ppa:jonathonf/python-3.6; fi
-  - if [[ $TRAVIS_PYTHON_VERSION == '3.7-dev' ]]; then sudo add-apt-repository -y ppa:deadsnakes/ppa; fi
-  - sudo apt-get update -q
-  - sudo apt-get remove -qy --purge lxd lxd-client
-  - sudo apt-get install snapd libsodium-dev -y
-install:
-  - sudo snap install lxd || true  # ignore failures so that unit tests will still run, at least
-  - sudo sh -c 'echo PATH=/snap/bin:$PATH >> /etc/environment';
-  - sudo snap install jq || true
-  - sudo snap install juju --classic --$JUJU_CHANNEL || true
-  - sudo snap install juju-wait --classic || true
-  - pip install tox-travis
-env:
-  global: >
-    TEST_AGENTS='{"agents":[{"url":"https://api.staging.jujucharms.com/identity","username":"libjuju-ci@yellow"}],"key":{"private":"88OOCxIHQNguRG7zFg2y2Hx5Ob0SeVKKBRnjyehverc=","public":"fDn20+5FGyN2hYO7z0rFUyoHGUnfrleslUNtoYsjNSs="}}'
-    PATH="/snap/bin:$PATH"
-    LXD_DIR="/var/snap/lxd/common/lxd"
-
-matrix:
-  include:
-  - python: 3.6
-    env: JUJU_CHANNEL=edge
-  - python: 3.6
-    env: JUJU_CHANNEL=stable
-  - python: 3.7-dev
-    env: JUJU_CHANNEL=stable
-  - python: 3.7-dev
-    env: JUJU_CHANNEL=edge
-before_script:
-  - set -e
-  # Run lint and unit tests before performing more expensive operations (fail fast/early)
-  - tox -e lint,py3
-
-  # init lxd for tests
-  - sudo lxd waitready --timeout 30
-  - sudo chmod 666 /var/snap/lxd/common/lxd/unix.socket
-  - lxd init --auto --network-address='[::]' --network-port=8443 --storage-backend=dir
-  - lxc network set lxdbr0 ipv6.address none
-
-  # Horrible workaround to LP Bug #1738614
-  - sudo mkdir /var/snap/lxd/common/lxd/storage-pools/juju-zfs
-  - lxc storage create juju-zfs dir source=/var/snap/lxd/common/lxd/storage-pools/juju-zfs
-
-script:
-  - set -e
-  - juju bootstrap localhost test --config 'identity-url=https://api.staging.jujucharms.com/identity' --config 'allow-model-access=true'
-  - tox -e integration,serial
diff --git a/juju/application.py b/juju/application.py
index ccc63e9..d94ab79 100644
--- a/juju/application.py
+++ b/juju/application.py
@@ -311,7 +311,7 @@ class Application(model.ModelEntity):
                 actions = result.actions
                 break
         if not schema:
-            actions = {k: v['description'] for k, v in actions.items()}
+            actions = {k: v.description for k, v in actions.items()}
         return actions
 
     async def get_resources(self):
diff --git a/juju/unit.py b/juju/unit.py
index 0456255..db619cc 100644
--- a/juju/unit.py
+++ b/juju/unit.py
@@ -279,25 +279,27 @@ class Unit(model.ModelEntity):
 
         # Is the application a subordinate? If so change our data variables to
         # the parent
-        if status.applications[app].get('subordinate-to'):
+        if status.applications[app].subordinate_to:
             is_subordinate = True
-            target_apps = status.applications[app]['subordinate-to']
+            target_apps = status.applications[app].subordinate_to
 
         for target_app in target_apps:
             app_data = status.applications[target_app]
 
-            if not app_data.get('units'):
+            if not app_data.units:
                 continue
 
-            if app_data['units'].get(self.name):
-                return app_data['units'][self.name].get('leader', False)
+            if app_data.units.get(self.name):
+                is_leader = app_data.units[self.name].leader
+                return is_leader if is_leader else False
 
             if not is_subordinate:
                 continue
 
-            for key, unit in app_data['units'].items():
-                if unit.get('subordinates') and unit['subordinates'].get(self.name):
-                    return unit['subordinates'][self.name].get('leader', False)
+            for key, unit in app_data.units.items():
+                if unit.subordinates and unit.subordinates.get(self.name):
+                    is_leader = unit.subordinates[self.name].leader
+                    return is_leader if is_leader else False
 
         return False
 
diff --git a/tox.ini b/tox.ini
index 29a7665..80cc2c3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -39,7 +39,6 @@ commands =
     py.test --tb native -ra -v -s -n auto -k 'not integration' -m 'not serial' {posargs}
 
 [testenv:lint]
-envdir = {toxworkdir}/py3
 commands =
     flake8 --ignore E501,W504,E402 {posargs} juju tests examples
 deps =
@@ -56,7 +55,11 @@ commands =
 [testenv:serial]
 # tests that can't be run in parallel
 envdir = {toxworkdir}/py3
-commands = py.test --tb native -ra -v -s {posargs:-m 'serial'}
+commands =
+    # These need to be installed in a specific order
+    pip install urllib3==1.25.7
+    pip install pylxd
+    py.test --tb native -ra -v -s {posargs:-m 'serial'}
 
 [testenv:example]
 envdir = {toxworkdir}/py3

travis ci broken?
every single commit in this repo for many moons and every single pr on this repo are failing status checks, which conveys a connotation of a project thats not really working. if your not using travis then remove the .travis.yml config from this repo or fix.
diff --git a/tests/integration/test_unit.py b/tests/integration/test_unit.py
index 95e932d..6983454 100644
--- a/tests/integration/test_unit.py
+++ b/tests/integration/test_unit.py
@@ -57,10 +57,25 @@ async def test_run_action(event_loop):
 
         for unit in app.units:
             action = await run_action(unit)
-            assert action.results == {'dir': '/var/git/myrepo.git'}
+            assert action.results == {
+                'Code': '0',
+                'Stdout': "Adding group `myrepo' (GID 1001) ...\n"
+                          'Done.\n'
+                          'Initialized empty Git repository in '
+                          '/var/git/myrepo.git/\n',
+                'dir': '/var/git/myrepo.git',
+            }
             out = await model.get_action_output(action.entity_id, wait=5)
-            assert out == {'dir': '/var/git/myrepo.git'}
-            status = await model.get_action_status(uuid_or_prefix=action.entity_id)
+            assert out == {
+                'Code': '0',
+                'Stdout': "Adding group `myrepo' (GID 1001) ...\n"
+                          'Done.\n'
+                          'Initialized empty Git repository in '
+                          '/var/git/myrepo.git/\n',
+                'dir': '/var/git/myrepo.git',
+            }
+            status = await model.get_action_status(
+                uuid_or_prefix=action.entity_id)
             assert status[action.entity_id] == 'completed'
             break
 
diff --git a/tests/unit/test_unit.py b/tests/unit/test_unit.py
index 527cc6e..e52fe36 100644
--- a/tests/unit/test_unit.py
+++ b/tests/unit/test_unit.py
@@ -4,6 +4,7 @@ import pytest
 
 from juju.model import Model
 from juju.unit import Unit
+from juju.client._definitions import FullStatus
 
 from .. import base
 
@@ -137,8 +138,7 @@ async def test_unit_is_leader(mock_cf):
     model._connector = mock.MagicMock()
 
     for test in tests:
-        status = mock.Mock()
-        status.applications = test['applications']
+        status = FullStatus.from_json(test)
         client_facade = mock_cf.from_connection()
         client_facade.FullStatus = base.AsyncMock(return_value=status)
 

pip install -e .
parse_log_pytest
pytest
asynctest ipdb mock pytest pytest-asyncio pytest-xdist
apt-get update apt-get install -y python3-all debhelper sbuild schroot ubuntu-dev-tools snapd libsodium-dev
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
asynctest==0.13.0
attrs @ file:///croot/attrs_1668696182826/work
backcall==0.2.0
bcrypt==4.2.1
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
charset-normalizer==3.4.1
cryptography==44.0.2
decorator==5.1.1
execnet==2.0.2
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
ipdb==0.13.13
ipython==7.34.0
jedi==0.19.2
-e git+https://github.com/juju/python-libjuju.git@9e1409a89cb8f6666b2204d15dcdd226d95031a1#egg=juju
jujubundlelib==0.5.7
macaroonbakery==1.3.4
matplotlib-inline==0.1.6
mock==5.2.0
mypy-extensions==1.0.0
packaging @ file:///croot/packaging_1671697413597/work
paramiko==2.12.0
parso==0.8.4
pexpect==4.9.0
pickleshare==0.7.5
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
prompt_toolkit==3.0.48
protobuf==4.24.4
ptyprocess==0.7.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyasn1==0.5.1
pycparser==2.21
Pygments==2.17.2
pymacaroons==0.13.0
PyNaCl==1.5.0
pyRFC3339==1.1
pytest==7.1.2
pytest-asyncio==0.21.2
pytest-xdist==3.5.0
pytz==2025.2
PyYAML==6.0
requests==2.31.0
six==1.17.0
theblues==0.5.2
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
toposort==1.10
traitlets==5.9.0
typing-inspect==0.9.0
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
urllib3==2.0.7
wcwidth==0.2.13
websockets==7.0
zipp @ file:///croot/zipp_1672387121353/work

name: python-libjuju
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - asynctest==0.13.0
      - backcall==0.2.0
      - bcrypt==4.2.1
      - cffi==1.15.1
      - charset-normalizer==3.4.1
      - cryptography==44.0.2
      - decorator==5.1.1
      - execnet==2.0.2
      - idna==3.10
      - ipdb==0.13.13
      - ipython==7.34.0
      - jedi==0.19.2
      - jujubundlelib==0.5.7
      - macaroonbakery==1.3.4
      - matplotlib-inline==0.1.6
      - mock==5.2.0
      - mypy-extensions==1.0.0
      - paramiko==2.12.0
      - parso==0.8.4
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - prompt-toolkit==3.0.48
      - protobuf==4.24.4
      - ptyprocess==0.7.0
      - pyasn1==0.5.1
      - pycparser==2.21
      - pygments==2.17.2
      - pymacaroons==0.13.0
      - pynacl==1.5.0
      - pyrfc3339==1.1
      - pytest-asyncio==0.21.2
      - pytest-xdist==3.5.0
      - pytz==2025.2
      - pyyaml==6.0
      - requests==2.31.0
      - six==1.17.0
      - theblues==0.5.2
      - toposort==1.10
      - traitlets==5.9.0
      - typing-inspect==0.9.0
      - urllib3==2.0.7
      - wcwidth==0.2.13
      - websockets==7.0
prefix: /opt/conda/envs/python-libjuju

tests/unit/test_unit.py::test_unit_is_leader
.github/workflows/tox.yaml juju/application.py .travis.yml juju/unit.py tox.ini
.github/workflows/tox.yaml juju/application.py .travis.yml juju/unit.py tox.ini