├── .circleci
└── config.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE.txt
├── README.md
├── RELEASING.md
├── build.gradle
├── busybee-android
├── .gitignore
├── build.gradle.kts
└── src
│ ├── androidTest
│ └── java
│ │ └── io
│ │ └── americanexpress
│ │ └── busybee
│ │ └── internal
│ │ ├── AndroidMainThreadExecutorTest.kt
│ │ └── BusyBeeSingletonTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── io
│ │ └── americanexpress
│ │ └── busybee
│ │ └── android
│ │ └── internal
│ │ ├── AndroidMainThreadExecutor.kt
│ │ ├── BusyBeeIdlingResource.kt
│ │ └── BusyBeeIdlingResourceRegistration.kt
│ └── test
│ └── java
│ └── io
│ └── americanexpress
│ └── busybee
│ └── internal
│ ├── AndroidMainThreadExecutorJvmTest.kt
│ └── BusyBeeSingletonJvmTest.kt
├── busybee-core
├── .gitignore
├── build.gradle.kts
└── src
│ ├── main
│ └── java
│ │ └── io
│ │ └── americanexpress
│ │ └── busybee
│ │ ├── BusyBee.kt
│ │ ├── BusyBeeExecutorWrapper.kt
│ │ └── internal
│ │ ├── BusyBeeSingleton.kt
│ │ ├── EnvironmentChecks.kt
│ │ ├── MainThread.kt
│ │ ├── NoOpBusyBee.kt
│ │ ├── RealBusyBee.kt
│ │ ├── Reflection.kt
│ │ └── SetMultiMap.kt
│ └── test
│ └── java
│ └── io
│ └── americanexpress
│ └── busybee
│ ├── BusyBeeTest.kt
│ └── internal
│ ├── BusyBeeExecutorWrapperTest.kt
│ ├── BusyBeeSingletonTest.kt
│ ├── EnvironmentChecksTest.kt
│ ├── MainThreadTest.kt
│ ├── NoOpBusyBeeTest.kt
│ ├── ReflectionTest.kt
│ └── SetMultiMapTest.kt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── images
└── busybee.png
├── sample-app
├── .gitignore
├── build.gradle.kts
└── src
│ ├── androidTest
│ └── java
│ │ └── io
│ │ └── americanexpress
│ │ └── busybee
│ │ ├── BusyBeeActivityTest.kt
│ │ └── BusyBeeCategoryTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── io
│ │ └── americanexpress
│ │ └── busybee
│ │ └── sample
│ │ └── BusyBeeActivity.kt
│ └── res
│ ├── layout
│ └── activity_busy_bee.xml
│ └── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
└── settings.gradle
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | executors:
4 | android-api29:
5 | docker:
6 | # https://hub.docker.com/layers/circleci/android/api-29/images/sha256-9fc0f34301182ba3730b1ca085e827927675168e57d412f6cc86256c7cd28e2d
7 | # api-29, JDK 11.0.10, commandlinetools-linux-6609375_latest.zip
8 | - image: circleci/android@sha256:9fc0f34301182ba3730b1ca085e827927675168e57d412f6cc86256c7cd28e2d
9 |
10 |
11 | anchors:
12 | - &common_job_config
13 | executor: android-api29
14 |
15 |
16 | jobs:
17 | test:
18 | <<: *common_job_config
19 | steps:
20 | # TODO: need to merge with destination branch before building
21 | - checkout
22 | - run:
23 | name: gradle test
24 | command: ./gradlew cleanTest test --stacktrace
25 |
26 | publish-snapshot:
27 | <<: *common_job_config
28 | steps:
29 | - checkout
30 | - run:
31 | name: Publish -SNAPSHOT
32 | command: MAVEN_REPO_URL="https://s01.oss.sonatype.org/content/repositories/snapshots/" ./gradlew build publish --stacktrace
33 |
34 | publish-release:
35 | <<: *common_job_config
36 | steps:
37 | - checkout
38 | - run:
39 | name: Publish Release
40 | command: MAVEN_REPO_URL="https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" ./gradlew build publish -Dsnapshot=false --stacktrace
41 |
42 |
43 | workflows_setup:
44 | - &context
45 | context:
46 | - maven_central_credentials
47 | - code_signing_credentials
48 |
49 | - &default_branch
50 | "main"
51 |
52 | workflows:
53 | pull_request_workflow:
54 | jobs:
55 | - test:
56 | filters:
57 | branches:
58 | ignore: *default_branch
59 |
60 | default_branch_workflow:
61 | when:
62 | # TODO: publish -SNAPSHOT for branches with branch name in the version
63 | equal: [ *default_branch, << pipeline.git.branch >> ]
64 | jobs:
65 | - test
66 | - publish-snapshot:
67 | <<: [ *context ]
68 | requires:
69 | - test
70 |
71 |
72 | publish_release:
73 | jobs:
74 | - publish-release:
75 | <<: [ *context ]
76 | filters:
77 | tags:
78 | only: /^\d+\.\d+\.\d+/
79 | branches:
80 | ignore: /.*/
81 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | /.circleci/processed.yml
10 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ### American Express Open Source Community Guidelines
2 |
3 | #### Last Modified: January 29, 2016
4 |
5 | Welcome to the American Express Open Source Community on GitHub! These American Express Community
6 | Guidelines outline our expectations for Github participating members within the American Express
7 | community, as well as steps for reporting unacceptable behavior. We are committed to providing a
8 | welcoming and inspiring community for all and expect our community Guidelines to be honored.
9 |
10 | **IMPORTANT REMINDER:**
11 |
12 | When you visit American Express on any third party sites such as GitHub your activity there is
13 | subject to that site’s then current terms of use., along with their privacy and data security
14 | practices and policies. The Github platform is not affiliated with us and may have practices and
15 | policies that are different than are our own.
16 |
17 | Please note, American Express is not responsible for, and does not control, the GitHub site’s terms
18 | of use, privacy and data security practices and policies. You should, therefore, always exercise
19 | caution when posting, sharing or otherwise taking any action on that site and, of course, on the
20 | Internet in general.
21 |
22 | Our open source community strives to:
23 | - **Be friendly and patient**.
24 | - **Be welcoming**: We strive to be a community that welcomes and supports people of all
25 | backgrounds and identities. This includes, but is not limited to members of any race, ethnicity,
26 | culture, national origin, color, immigration status, social and economic class, educational level,
27 | sex, sexual orientation, gender identity and expression, age, size, family status, political belief,
28 | religion, and mental and physical ability.
29 | - **Be considerate**: Your work will be used by other people, and you in turn will depend on the
30 | work of others. Any decision you take will affect users and colleagues, and you should take those
31 | consequences into account when making decisions. Remember that we're a world-wide community, so you
32 | might not be communicating in someone else's primary language.
33 | - **Be respectful**: Not all of us will agree all the time, but disagreement is no excuse for poor
34 | behavior and poor manners. We might all experience some frustration now and then, but we cannot
35 | allow that frustration to turn into a personal attack. It’s important to remember that a community
36 | where people feel uncomfortable or threatened is not a productive one.
37 | - **Be careful in the words that we choose**: We are a community of professionals, and we conduct
38 | ourselves professionally. Be kind to others. Do not insult or put down other participants.
39 | Harassment and other exclusionary behavior aren't acceptable.
40 | - **Try to understand why we disagree**: Disagreements, both social and technical, happen all the
41 | time. It is important that we resolve disagreements and differing views constructively. Remember
42 | that we’re all different people. The strength of our community comes from its diversity, people from
43 | a wide range of backgrounds. Different people have different perspectives on issues. Being unable to
44 | understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is
45 | human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve
46 | issues and learning from mistakes.
47 |
48 | ### Definitions
49 | Harassment includes, but is not limited to:
50 | - Offensive comments related to gender, gender identity and expression, sexual orientation,
51 | disability, mental illness, neuro(a)typicality, physical appearance, body size, race, age, regional
52 | discrimination, political or religious affiliation
53 | - Unwelcome comments regarding a person’s lifestyle choices and practices, including those related
54 | to food, health, parenting, drugs, and employment
55 | - Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not
56 | correctly reflect a person's gender identity. You must address people by the name they give you when
57 | not addressing them by their username or handle
58 | - Physical contact and simulated physical contact (eg, textual descriptions like “hug” or “backrub”)
59 | without consent or after a request to stop
60 | - Threats of violence, both physical and psychological
61 | - Incitement of violence towards any individual, including encouraging a person to commit suicide
62 | or to engage in self-harm
63 | - Deliberate intimidation
64 | - Stalking or following
65 | - Harassing photography or recording, including logging online activity for harassment purposes
66 | - Sustained disruption of discussion
67 | - Unwelcome sexual attention, including gratuitous or off-topic sexual images or behaviour
68 | - Pattern of inappropriate social contact, such as requesting/assuming inappropriate levels of
69 | intimacy with others
70 | - Continued one-on-one communication after requests to cease
71 | - Deliberate “outing” of any aspect of a person’s identity without their consent except as necessary
72 | to protect others from intentional abuse
73 | - Publication of non-harassing private communication
74 |
75 | Our open source community prioritizes marginalized people’s safety over privileged people’s comfort.
76 | We will not act on complaints regarding:
77 | - ‘Reverse’ -isms, including ‘reverse racism,’ ‘reverse sexism,’ and ‘cisphobia’
78 | - Reasonable communication of boundaries, such as “leave me alone,” “go away,” or “I’m not
79 | discussing this with you”
80 | - Refusal to explain or debate social justice concepts
81 | - Communicating in a ‘tone’ you don’t find congenial
82 | - Criticizing racist, sexist, cissexist, or otherwise oppressive behavior or assumptions
83 |
84 | ### Diversity Statement
85 | We encourage everyone to participate and are committed to building a community for all. Although we
86 | will fail at times, we seek to treat everyone both as fairly and equally as possible. Whenever a
87 | participant has made a mistake, we expect them to take responsibility for it. If someone has been
88 | harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best
89 | to right the wrong.
90 |
91 | Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender
92 | identity or expression, culture, ethnicity, language, national origin, political beliefs,
93 | profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will
94 | not tolerate discrimination based on any of the protected characteristics above, including
95 | participants with disabilities.
96 |
97 | ### Reporting Issues
98 | If you experience or witness unacceptable behavior—or have any other concerns—please report it by
99 | contacting us at opensource@aexp.com. All reports will be handled with discretion. In your report
100 | please include:
101 | - Your contact information.
102 | - Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional
103 | witnesses, please include them as well. Your account of what occurred, and if you believe the
104 | incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a
105 | public IRC logger), please include a link.
106 | - Any additional information that may be helpful.
107 |
108 | After filing a report, a representative of our community will contact you personally, review the
109 | incident, follow up with any additional questions, and make a decision as to how to respond. If the
110 | person who is harassing you is part of the response team, they will recuse themselves from handling
111 | your incident. If the complaint originates from a member of the response team, it will be handled by
112 | a different member of the response team. We will respect confidentiality requests for the purpose of
113 | protecting victims of abuse.
114 |
115 | ### Removal of Posts
116 | We will not review every comment or post, but we reserve the right to remove any that violates these
117 | Guidelines or that, in our sole discretion, we otherwise consider objectionable and we may ban
118 | offenders from our community.
119 |
120 | ### Suspension/Termination/Reporting to Authority
121 | In certain instances, we may suspend, terminate or ban certain repeat offenders and/or those
122 | committing significant violations of these Guidelines. When appropriate, we may also, on our own or
123 | as required by the GitHub terms of use, be required to refer and/or work with GitHub and/or the
124 | appropriate authorities to review and/or pursue certain violations.
125 |
126 | ### Attribution & Acknowledgements
127 | These Guidelines have been adapted from the
128 | [Code of Conduct of the TODO group](http://todogroup.org/opencodeofconduct/). They are subject to
129 | revision by American Express and may be revised from time to time.
130 |
131 | Thank you for your participation!
132 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2019 American Express Travel Related Services Company, Inc.
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BusyBee - Tell Espresso when it needs to be patient because your app is busy 🐝
2 |
3 |
4 |
5 | BusyBee is an alternative API for [IdlingResource][]s in [Espresso][] tests. You can use BusyBee instead of
6 | [CountingIdlingResource][] to get better log messages and improve your ability to debug problems related to
7 | [IdlingResource][]s.
8 |
9 | BusyBee is meant to be used with [Espresso][]. You use BusyBee inside the "app under test". It allows the "app under
10 | test" to tell Espresso when it is `busyWith` an operation and, conversely, allows the app to tell Espresso when the
11 | operation is `completed`. Tracking `busyWith`/`completed` helps your **Espresso tests be fast and reliable**.
12 |
13 | If you write [Espresso][] tests, proper use of the [IdlingResource][] API is critical for ensuring that your tests are
14 | fast and reliable. IdlingResource can be hard to use correctly and it can be hard to understand what is happening with
15 | your IdlingResources when you are debugging problems with your tests. That is where BusyBee comes in.
16 |
17 | ## Comparison with [CountingIdlingResource][]
18 |
19 | In some ways, BusyBee is similar to [CountingIdlingResource][], but it does have some notable advantages:
20 |
21 | - Rather than track only the _number_ of operations in progress, BusyBee keeps track of the set of operations currently
22 | in progress. In progress operations are represented by a Java object, which could be a string, request object, etc.
23 | This allows for easier debugging, as it allows you to inspect the set of in progress operations across the whole app.
24 | - When Espresso times out because the app is busy, your logs can show the list of in progress operations.
25 | - BusyBee lets you separately enable/disable tracking of specific categories of operations (e.g. `NETWORK` operations)
26 | - The `BusyBee#completed(thing)` method is idempotent (`CountingIdlingResource#decrement` is not). This is useful when
27 | you have unreliable/multiple signals (e.g. WebView) to tell you that an operation has completed. Also, you can
28 | `completed(thing)` even if you never were `busyWith(thing)`
29 |
30 | **Trade-off:** While there are a number of advantages listed above, the downside of `BusyBee` (and
31 | `CountingIdlingResource`) is that you are modifying your app under test for purely testing purposes.
32 |
33 | # How to use BusyBee
34 |
35 | Include the BusyBee dependencies in your `build.gradle` files. When tests are not running, the **no-op** implementation
36 | is automatically used to minimize overhead of BusyBee (since it is only needed during tests).
37 |
38 | [Find the latest version on Maven Central](https://search.maven.org/artifact/io.americanexpress.busybee/busybee-core)
39 |
40 | _Required_: For Android modules:
41 |
42 | ```gradle
43 | implementation 'io.americanexpress.busybee:busybee-android:$version'
44 | ```
45 |
46 | _Optional_: Only needed, if you want to use BusyBee in a non-Android module:
47 |
48 | ```gradle
49 | implementation 'io.americanexpress.busybee:busybee-core:$version'
50 | ```
51 |
52 | BusyBee releases are available on Maven Central.
53 |
54 | ```gradle
55 | repositories {
56 | exclusiveContent {
57 | forRepository { mavenCentral() }
58 | filter { includeGroup("io.americanexpress.busybee") }
59 | }
60 | }
61 | ```
62 |
63 | On each merge to `main`, a `-SNAPSHOT` version is published to https://s01.oss.sonatype.org/content/repositories/snapshots/
64 |
65 | Inside your _app_, tell `BusyBee` what operations your app is `busyWith`, and when that operation is `completed`.
66 |
67 | ```java
68 | class BackgroundProcessor {
69 | private final BusyBee busyBee = BusyBee.singleton();
70 |
71 | void processThing(Thing thing){
72 | // Espresso will wait
73 | busyBee.busyWith(thing);
74 | try {
75 | thing.process();
76 | } finally {
77 | // Espresso will continue
78 | busyBee.completed(thing);
79 | }
80 | }
81 | }
82 | ```
83 |
84 | That's all! Now Espresso will wait until your app is not busy before executing its actions and assertions.
85 |
86 | ## Categories
87 |
88 | Assigning a `Category` to your operations is an advanced feature of `BusyBee`. By default, all operations are in the
89 | `GENERAL` category. But, you can also add operations in other categories such as `NETWORK`. You can toggle tracking for
90 | any category with `payAttentionToCategory`/`ignoreCategory`. When a category is being "ignored" then Espresso will not
91 | wait for operations in that category.
92 |
93 | For example, you might want to perform actions on your UI or assert things about your UI while a network request is
94 | still in progress. In this case, you don't want Espresso to wait for the network requests to complete, but you still
95 | want Espresso to wait for other operations in your app. To accomplish this, you would use
96 | `busyBee.ignoreCategory(NETWORK)`, then perform actions and assertions on your UI, then call
97 | `busybee.payAttentionToCategory(NETWORK)` so Espresso will again wait for network operations to complete.
98 |
99 | ## BusyBeeExecutorWrapper
100 |
101 | If you have an executor and you need Espresso to know the app is "busy" anytime that executor is executing something,
102 | then you can wrap the `Executor` with `BusyBeeExecutorWrapper`. Operations executed with the wrapped `Executor` will
103 | cause `BusyBee` to be "busy" while they are in progress.
104 |
105 | ```java
106 | Executor backgroundTasks;
107 | Executor busyBeeBackgroundTasks =
108 | BusyBeeExecutorWrapper.with(busyBee)
109 | .wrapExecutor(backgroundTasks)
110 | .build();
111 | busyBeeBackgroundTasks.execute(operation);
112 | ```
113 |
114 | ## Contributing
115 |
116 | We welcome Your interest in the American Express Open Source Community on Github. Any Contributor to any Open Source
117 | Project managed by the American Express Open Source Community must accept and sign an Agreement indicating agreement to
118 | the terms below. Except for the rights granted in this Agreement to American Express and to recipients of software
119 | distributed by American Express, You reserve all right, title, and interest, if any, in and to Your Contributions.
120 | Please [fill out the Agreement][].
121 |
122 | ## License
123 |
124 | Any contributions made under this project will be governed by the [Apache License 2.0][].
125 |
126 | The Android™ robot is reproduced or modified from work created and shared by Google and used according to terms
127 | described in the Creative Commons 3.0 Attribution License. Android is a trademark of Google Inc.
128 |
129 | ## Code of Conduct
130 |
131 | This project adheres to the [American Express Community Guidelines][]. By participating, you are expected to honor these
132 | guidelines.
133 |
134 | [espresso]: https://developer.android.com/training/testing/espresso
135 | [idlingresource]: https://developer.android.com/reference/androidx/test/espresso/IdlingResource
136 | [countingidlingresource]: https://developer.android.com/reference/androidx/test/espresso/idling/CountingIdlingResource
137 | [fill out the agreement]: https://cla-assistant.io/americanexpress/busybee
138 | [apache license 2.0]: https://github.com/americanexpress/busybee/blob/main/LICENSE.txt
139 | [american express community guidelines]: https://github.com/americanexpress/busybee/blob/main/CODE_OF_CONDUCT.md
140 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | ## RELEASE INSTRUCTIONS
2 |
3 | For a manual release:
4 |
5 | 1. Make sure you have pulled the latest changes with `git pull`
6 | 2. Make sure you have no uncommitted changes with `git status`
7 | 3. Create a git tag with the version from `gradle.properties`. e.g. `git tag 1.0.1`
8 | 4. Push the tag. i.e. `git push --tags`
9 | 5. Build and publish the artifacts: `MAVEN_REPO_URL= ./gradlew publish -Dsnapshot=false`
10 | 6. Open a PR to increment `version` in `gradle.properties` to the next version
11 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | import java.nio.charset.Charset
2 |
3 | /*
4 | * Copyright 2020 American Express Travel Related Services Company, Inc.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License
12 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13 | * or implied. See the License for the specific language governing permissions and limitations under
14 | * the License.
15 | */
16 | buildscript {
17 | ext.kotlin_version = '1.6.0'
18 | repositories {
19 | mavenCentral()
20 | exclusiveContent {
21 | forRepository { google() }
22 | filter {
23 | includeGroupByRegex("androidx\\..*")
24 | includeGroupByRegex("com\\.android\\..*")
25 | includeGroup("com.android")
26 | includeGroup("com.google.testing.platform")
27 | includeGroup("com.google.android.material")
28 | }
29 | }
30 | }
31 | dependencies {
32 | classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.6.0'
33 | classpath 'com.android.tools.build:gradle:7.0.4'
34 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
35 | }
36 | }
37 |
38 | allprojects {
39 | repositories {
40 | mavenCentral()
41 | exclusiveContent {
42 | forRepository { google() }
43 | filter {
44 | includeGroupByRegex("androidx\\..*")
45 | includeGroupByRegex("com\\.android\\..*")
46 | includeGroup("com.android")
47 | includeGroup("com.google.testing.platform")
48 | includeGroup("com.google.android.material")
49 | }
50 | }
51 | }
52 | }
53 |
54 | def snapshot = System.getProperty("snapshot", "true").toBoolean()
55 | group = System.getProperty('group')
56 | version = System.getProperty('version') + (snapshot ? '-SNAPSHOT' : '')
57 |
58 | subprojects {
59 | ext.deps = [targetSdk : 29,
60 | compileSdk: 29,
61 | minSdk : 19,
62 | junit : '4.13.1',
63 | assertj : '2.6.0',
64 | mockito2 : '3.0.0',
65 | androidx : [
66 | material : '1.0.0',
67 | annotation: '1.1.0',
68 | test : [
69 | rules : '1.2.0',
70 | espresso: [
71 | core : '3.2.0',
72 | idling_resource: '3.2.0'
73 | ]
74 | ]
75 | ]
76 | ]
77 |
78 | ext.compilerArgs = ["-Werror"]
79 |
80 | apply plugin: 'org.jetbrains.dokka'
81 | apply plugin: 'maven-publish'
82 | apply plugin: 'signing'
83 |
84 | group = rootProject.group
85 | version = rootProject.version
86 |
87 | afterEvaluate {
88 | if (plugins.hasPlugin('java-library')) {
89 | sourceCompatibility = 1.8
90 | targetCompatibility = 1.8
91 | }
92 |
93 | if (plugins.hasPlugin('org.jetbrains.kotlin.android')) {
94 | android.kotlinOptions {
95 | jvmTarget = "1.8"
96 | }
97 | }
98 |
99 | if (plugins.hasPlugin('org.jetbrains.kotlin.jvm')) {
100 | compileKotlin {
101 | kotlinOptions {
102 | jvmTarget = "1.8"
103 | }
104 | }
105 |
106 | compileTestKotlin {
107 | kotlinOptions {
108 | jvmTarget = "1.8"
109 | }
110 | }
111 | java {
112 | withSourcesJar()
113 | }
114 | }
115 |
116 | if (plugins.hasPlugin('com.android.library')
117 | || plugins.hasPlugin('com.android.application')) {
118 | android {
119 |
120 | lintOptions {
121 | disable "GradleDependency"
122 | }
123 |
124 | compileOptions {
125 | sourceCompatibility JavaVersion.VERSION_1_8
126 | targetCompatibility JavaVersion.VERSION_1_8
127 | }
128 | }
129 | }
130 |
131 | // Use getenv so they can be passed in without having them in a file
132 | // or in the command args
133 | def maven_username = System.getenv('MAVEN_CENTRAL_USERNAME') ?: ''
134 | def maven_password = System.getenv('MAVEN_CENTRAL_PASSWORD') ?: ''
135 |
136 | // from https://docs.gradle.org/current/userguide/publishing_maven.html
137 | if (plugins.hasPlugin('java-library')) {
138 | publishing {
139 | task dokkaJavadocJar(type: Jar) {
140 | from dokkaJavadoc.outputDirectory
141 | }
142 | repositories {
143 | maven {
144 | // Use getenv so they can be passed in without having them in a file
145 | // or in the command args
146 | credentials {
147 | username maven_username
148 | password maven_password
149 | }
150 |
151 | url System.getenv('MAVEN_REPO_URL')
152 | }
153 | }
154 | publications {
155 | // groupId and version come from gradle.properties
156 | // artifactId comes from the name of the gradle module
157 | busybee(MavenPublication) {
158 | from components.java
159 | artifact dokkaJavadocJar {
160 | getArchiveClassifier().set('javadoc')
161 | }
162 | pom {
163 | name = 'busybee'
164 | description = 'BusyBee is an alternative API for IdlingResources in Espresso tests.'
165 | url = 'https://github.com/americanexpress/busybee'
166 | licenses {
167 | license {
168 | name = 'The Apache License, Version 2.0'
169 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
170 | }
171 | }
172 | developers {
173 | developer {
174 | name = 'American Express Travel Related Services Company, Inc.'
175 | }
176 | }
177 | scm {
178 | connection = 'scm:git:https://github.com/americanexpress/busybee.git'
179 | developerConnection = 'scm:git:git@github.com:americanexpress/busybee.git'
180 | url = 'https://github.com/americanexpress/busybee'
181 | }
182 | }
183 | }
184 | }
185 | }
186 | def signingKeyBase64 = findProperty("signingKey_base64")
187 | if (signingKeyBase64 != null) {
188 | signing {
189 | def signingKey = new String(Base64.decoder.decode(signingKeyBase64), Charset.forName("UTF-8"))
190 | def signingPassword = findProperty("signingPassword")
191 | useInMemoryPgpKeys(signingKey, signingPassword)
192 | sign publishing.publications.busybee
193 | }
194 | }
195 | }
196 |
197 | if (plugins.hasPlugin('com.android.library')) {
198 | // need afterEvaluate else the components.release will be missing below
199 | afterEvaluate {
200 | // need this until it is built into AGP, see https://issuetracker.google.com/issues/145670440
201 | task androidSourcesJar(type: Jar) {
202 | from android.sourceSets.main.java.srcDirs
203 | }
204 | task dokkaJavadocJar(type: Jar) {
205 | from dokkaJavadoc.outputDirectory
206 | }
207 | publishing {
208 | repositories {
209 | maven {
210 | credentials {
211 | username maven_username
212 | password maven_password
213 | }
214 |
215 | url System.getenv('MAVEN_REPO_URL')
216 | }
217 | }
218 | publications {
219 | // groupId and version come from gradle.properties
220 | // artifactId comes from the name of the gradle module
221 | busybee(MavenPublication) {
222 | from components.release
223 | artifact androidSourcesJar {
224 | getArchiveClassifier().set('sources')
225 | }
226 | artifact dokkaJavadocJar {
227 | getArchiveClassifier().set('javadoc')
228 | }
229 | pom {
230 | name = 'busybee'
231 | description = 'BusyBee is an alternative API for IdlingResources in Espresso tests.'
232 | url = 'https://github.com/americanexpress/busybee'
233 | licenses {
234 | license {
235 | name = 'The Apache License, Version 2.0'
236 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
237 | }
238 | }
239 | developers {
240 | developer {
241 | name = 'American Express Travel Related Services Company, Inc.'
242 | }
243 | }
244 | scm {
245 | connection = 'scm:git:https://github.com/americanexpress/busybee.git'
246 | developerConnection = 'scm:git:git@github.com:americanexpress/busybee.git'
247 | url = 'https://github.com/americanexpress/busybee'
248 | }
249 | }
250 | }
251 | }
252 | def signingKeyBase64 = findProperty("signingKey_base64")
253 | if (signingKeyBase64 != null) {
254 | signing {
255 | def signingKey = new String(Base64.decoder.decode(signingKeyBase64), Charset.forName("UTF-8"))
256 | def signingPassword = findProperty("signingPassword")
257 | useInMemoryPgpKeys(signingKey, signingPassword)
258 | sign publishing.publications.busybee
259 | }
260 | }
261 | }
262 | }
263 | }
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/busybee-android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/busybee-android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | plugins {
15 | id("com.android.library")
16 | id("org.jetbrains.kotlin.android")
17 | }
18 |
19 | android {
20 | compileSdkVersion = libs.versions.compileSdk.get()
21 | defaultConfig {
22 | minSdk = libs.versions.minimumSdk.get().toInt()
23 | targetSdk = libs.versions.targetSdk.get().toInt()
24 | version = 1
25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
26 | }
27 | buildTypes {
28 | getByName("release") {
29 | isMinifyEnabled = false
30 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
31 | }
32 | }
33 | }
34 |
35 |
36 | dependencies {
37 | api(project(":busybee-core"))
38 | implementation(libs.kotlin.stdlib)
39 | implementation(libs.androidx.espresso.idling.resource)
40 |
41 | androidTestImplementation(libs.assertj.core)
42 | androidTestImplementation(libs.junit4)
43 | androidTestImplementation(libs.androidx.espresso.core)
44 |
45 | testImplementation(libs.assertj.core)
46 | testImplementation(libs.junit4)
47 | }
48 |
--------------------------------------------------------------------------------
/busybee-android/src/androidTest/java/io/americanexpress/busybee/internal/AndroidMainThreadExecutorTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 |
15 | package io.americanexpress.busybee.internal
16 |
17 | import io.americanexpress.busybee.android.internal.AndroidMainThreadExecutor
18 | import io.americanexpress.busybee.internal.EnvironmentChecks.hasWorkingAndroidMainLooper
19 | import org.assertj.core.api.Java6Assertions.assertThat
20 | import org.junit.Test
21 |
22 | class AndroidMainThreadExecutorTest {
23 | @Test
24 | fun whenHasWorkingAndroidMainLooper_thenWeGetAndroidMainThreadExecutor() {
25 | assertThat(MainThread.singletonExecutor)
26 | .isInstanceOf(AndroidMainThreadExecutor::class.java)
27 | }
28 |
29 | @Test
30 | fun whenHasWorkingAndroidMainLooper_thenIsAndroidShouldBeTrue() {
31 | assertThat(hasWorkingAndroidMainLooper()).isTrue()
32 | }
33 | }
--------------------------------------------------------------------------------
/busybee-android/src/androidTest/java/io/americanexpress/busybee/internal/BusyBeeSingletonTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 |
15 | package io.americanexpress.busybee.internal
16 |
17 | import io.americanexpress.busybee.internal.EnvironmentChecks.androidJunitRunnerIsPresent
18 | import io.americanexpress.busybee.internal.EnvironmentChecks.junit4IsPresent
19 | import org.assertj.core.api.Java6Assertions.assertThat
20 | import org.junit.Test
21 |
22 | class BusyBeeSingletonTest {
23 |
24 | @Test
25 | fun whenTestsAreRunning_thenWeDetectJunit4() {
26 | assertThat(junit4IsPresent())
27 | .`as`("We are running tests, so this should be true")
28 | .isTrue()
29 | }
30 |
31 | @Test
32 | fun whenInAndroidTests_thenWeDetectItsAndroidRunner() {
33 | assertThat(androidJunitRunnerIsPresent())
34 | .`as`("We are running /androidTest, so this should be true")
35 | .isTrue()
36 | }
37 |
38 | @Test
39 | fun whenTestsAreRunning_thenWeUseRealBusyBee() {
40 | assertThat(BusyBeeSingleton.singleton())
41 | .`as`("We are running tests, so must use RealBusyBee")
42 | .isInstanceOf(RealBusyBee::class.java)
43 | }
44 | }
--------------------------------------------------------------------------------
/busybee-android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
17 |
18 |
19 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/busybee-android/src/main/java/io/americanexpress/busybee/android/internal/AndroidMainThreadExecutor.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.android.internal
15 |
16 | import android.os.Handler
17 | import android.os.Looper
18 | import java.util.concurrent.Executor
19 |
20 | enum class AndroidMainThreadExecutor : Executor {
21 | // accessed via reflection in busybee-core
22 | INSTANCE;
23 |
24 | private val handler = Handler(Looper.getMainLooper())
25 | override fun execute(command: Runnable) {
26 | handler.post(command)
27 | }
28 | }
--------------------------------------------------------------------------------
/busybee-android/src/main/java/io/americanexpress/busybee/android/internal/BusyBeeIdlingResource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.android.internal
15 |
16 | import androidx.test.espresso.IdlingResource
17 | import androidx.test.espresso.IdlingResource.ResourceCallback
18 | import io.americanexpress.busybee.BusyBee
19 |
20 | /**
21 | * This class is a bridge between espresso's IdlingResource and the app, so the app doesn't have to depend on espresso.
22 | *
23 | *
24 | * You must register it with Espresso:
25 | * `IdlingRegistry.getInstance().register(new BusyBeeIdlingResource(BusyBee.singleton()));`
26 | */
27 | class BusyBeeIdlingResource internal constructor(private val busyBee: BusyBee) : IdlingResource {
28 | override fun getName() = busyBee.getName()
29 |
30 | override fun isIdleNow() = busyBee.isNotBusy()
31 |
32 | override fun registerIdleTransitionCallback(resourceCallback: ResourceCallback) {
33 | busyBee.registerNoLongerBusyCallback { resourceCallback.onTransitionToIdle() }
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/busybee-android/src/main/java/io/americanexpress/busybee/android/internal/BusyBeeIdlingResourceRegistration.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.android.internal
15 |
16 | import android.content.ContentProvider
17 | import android.content.ContentValues
18 | import android.database.Cursor
19 | import android.net.Uri
20 | import androidx.test.espresso.IdlingRegistry
21 | import io.americanexpress.busybee.BusyBee
22 | import io.americanexpress.busybee.internal.EnvironmentChecks
23 |
24 | /**
25 | * We register BusyBeeIdlingResource with espresso when the application loads.
26 | *
27 | *
28 | * Auto-registration inspired by LeakCanary
29 | * https://github.com/square/leakcanary/blob/934174edd0c04f2937733aae4a01f836c67b5b52/leakcanary-object-watcher-android/src/main/java/leakcanary/internal/AppWatcherInstaller.kt
30 | */
31 | class BusyBeeIdlingResourceRegistration : ContentProvider() {
32 | override fun onCreate(): Boolean {
33 | if (EnvironmentChecks.testsAreRunning()) {
34 | IdlingRegistry.getInstance().register(BusyBeeIdlingResource(BusyBee.singleton()))
35 | }
36 | return true
37 | }
38 |
39 | override fun delete(
40 | uri: Uri,
41 | selection: String?,
42 | selectionArgs: Array?
43 | ) = 0
44 |
45 | override fun getType(uri: Uri): String? = null
46 |
47 | override fun insert(uri: Uri, values: ContentValues?): Uri? = null
48 |
49 | override fun query(
50 | uri: Uri, projection: Array?, selection: String?,
51 | selectionArgs: Array?, sortOrder: String?
52 | ): Cursor? = null
53 |
54 | override fun update(
55 | uri: Uri, values: ContentValues?, selection: String?,
56 | selectionArgs: Array?
57 | ) = 0
58 | }
--------------------------------------------------------------------------------
/busybee-android/src/test/java/io/americanexpress/busybee/internal/AndroidMainThreadExecutorJvmTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 |
15 | package io.americanexpress.busybee.internal
16 |
17 | import io.americanexpress.busybee.android.internal.AndroidMainThreadExecutor
18 | import io.americanexpress.busybee.internal.EnvironmentChecks.hasWorkingAndroidMainLooper
19 | import org.assertj.core.api.Java6Assertions.assertThat
20 | import org.junit.Test
21 |
22 | class AndroidMainThreadExecutorJvmTest {
23 | @Test
24 | fun whenDoesNotHaveWorkingAndroidMainLooper_thenWeDoNotGetAndroidMainThreadExecutor() {
25 | assertThat(MainThread.singletonExecutor)
26 | .isNotInstanceOf(AndroidMainThreadExecutor::class.java)
27 | }
28 |
29 | @Test
30 | fun whenDoesNotHaveWorkingAndroidMainLooper_thenHasWorkingAndroidMainLooperShouldBeFalse() {
31 | assertThat(hasWorkingAndroidMainLooper()).isFalse()
32 | }
33 | }
--------------------------------------------------------------------------------
/busybee-android/src/test/java/io/americanexpress/busybee/internal/BusyBeeSingletonJvmTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 |
15 | package io.americanexpress.busybee.internal
16 |
17 | import io.americanexpress.busybee.internal.EnvironmentChecks.androidJunitRunnerIsPresent
18 | import io.americanexpress.busybee.internal.EnvironmentChecks.junit4IsPresent
19 | import org.assertj.core.api.Java6Assertions.assertThat
20 | import org.junit.Test
21 |
22 | class BusyBeeSingletonJvmTest {
23 |
24 | @Test
25 | fun whenJunit4IsUsed_thenWeDetectItsPresence() {
26 | assertThat(junit4IsPresent())
27 | .`as`("We are running Junit4 tests, so this should be true")
28 | .isTrue()
29 | }
30 |
31 | @Test
32 | fun whenRunningJvmTestsWithoutEspresso_thenWeDontDetectIt() {
33 | assertThat(androidJunitRunnerIsPresent())
34 | .`as`("We are in /test without espresso, so this should be false")
35 | .isFalse()
36 | }
37 |
38 | @Test
39 | fun whenTestsAreRunning_thenWeUseRealBusyBee() {
40 | assertThat(BusyBeeSingleton.singleton())
41 | .`as`("We are running tests, so must use RealBusyBee")
42 | .isInstanceOf(RealBusyBee::class.java)
43 | }
44 | }
--------------------------------------------------------------------------------
/busybee-core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/busybee-core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | plugins {
15 | `java-library`
16 | id("org.jetbrains.kotlin.jvm")
17 | }
18 |
19 | dependencies {
20 | implementation(libs.androidx.annotation)
21 | // Android Studio 3.6 thinks we are using a different version of kotlin here, but we are not
22 | //noinspection DifferentStdlibGradleVersion
23 | implementation(libs.kotlin.stdlib)
24 |
25 | testImplementation(libs.assertj.core)
26 | testImplementation(libs.mockito.core)
27 | testImplementation(libs.junit4)
28 | }
29 |
--------------------------------------------------------------------------------
/busybee-core/src/main/java/io/americanexpress/busybee/BusyBee.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee
15 |
16 | import io.americanexpress.busybee.internal.BusyBeeSingleton
17 |
18 | interface BusyBee {
19 | fun getName(): String
20 |
21 | /**
22 | * Tell BusyBee that a new operation is now keeping the app "busy".
23 | * The operation that is passed in represents the ongoing operation.
24 | * Espresso will wait until `operation` is [.completed] (or an object that
25 | * is [Object.equals] to `operation` is completed.
26 | *
27 | * This operation will be placed in the [Category.defaultCategory].
28 | *
29 | * @param operation An object that identifies the operation that is keeping the app "busy".
30 | * Must have a correct implementation of [Object.equals]/[Object.hashCode].
31 | * Also, should have a meaningful [Object.toString].
32 | */
33 | fun busyWith(operation: Any)
34 |
35 | /**
36 | * Record the start of an async operation.
37 | *
38 | * @param operation An object that identifies the request. Must have a correct equals()/hashCode().
39 | * @param category Which [Category] the given operation will be associated with
40 | */
41 | fun busyWith(operation: Any, category: Category)
42 |
43 | /**
44 | * Get notified every time this BusyBee instance goes from being `busyWith` to no longer busy.
45 | *
46 | * @param noLongerBusyCallback callback
47 | */
48 | fun registerNoLongerBusyCallback(noLongerBusyCallback: NoLongerBusyCallback)
49 |
50 | /**
51 | * If there are any operations in progress in the given category, then BusyBee will be in the busy state.
52 | * By default, BusyBee "pays attention" to all categories
53 | *
54 | * @param category - Category to stop ignoring
55 | * @see BusyBee.ignoreCategory
56 | */
57 | fun payAttentionToCategory(category: Category)
58 |
59 | /**
60 | * If there are any operations in progress in the given category, then BusyBee will ignore those operations.
61 | * By default, BusyBee DOES NOT ignore any categories.
62 | *
63 | *
64 | * You can reverse this by calling [BusyBee.payAttentionToCategory]
65 | *
66 | * @param category - Category to be ignored
67 | * @see BusyBee.payAttentionToCategory
68 | */
69 | fun ignoreCategory(category: Category)
70 |
71 | /**
72 | * All operations in progress in this category will be marked as completed (i.e. no longer busy)
73 | *
74 | * @param category Everything in this category will be completed.
75 | * @see BusyBee.busyWith
76 | */
77 | fun completedEverythingInCategory(category: Category)
78 |
79 | /**
80 | * All operations in progress in all categories will be marked as completed (i.e. no longer busy)
81 | *
82 | * @see BusyBee.busyWith
83 | */
84 | fun completedEverything()
85 |
86 | /**
87 | * `complete` all operations for which [OperationMatcher.matches] returns true
88 | *
89 | * @param matcher - Logic for selecting which operations will be completed
90 | * @see OperationMatcher
91 | */
92 | fun completedEverythingMatching(matcher: OperationMatcher)
93 |
94 | /**
95 | * Marks an operation as complete, but the completion is done asynchronously (returns immediately) on the MainThread.
96 | * This means completion of the operation won't happen until this makes it to the front of the main thread queue.
97 | *
98 | *
99 | * The `operation` passed into this method should be something that was previously passed to [.busyWith]
100 | * If BusyBee wasn't already tracking the `operation` (either because it was already passed to completed()
101 | * or it was never [.busyWith] in the first place), then this method will have no effect.
102 | *
103 | * More info about the Android "main thread": https://www.youtube.com/watch?v=eAtMon8ndfk
104 | *
105 | * @param operation - Must have compliant implementations of `equals` and `hashcode`
106 | */
107 | fun completed(operation: Any)
108 |
109 | /**
110 | * No operations that we are paying attention to is currently in progress
111 | *
112 | * @return true if and only if we are not busyWith an operation
113 | */
114 | fun isNotBusy(): Boolean
115 |
116 | /**
117 | * @return true if and only if there are operations in progress (i.e. `busyWith`) for categories
118 | * we are "paying attention" to.
119 | * @see BusyBee.payAttentionToCategory
120 | * @see BusyBee.ignoreCategory
121 | */
122 | fun isBusy(): Boolean
123 |
124 | /**
125 | * Dumps the state of this BusyBee instance, that can be used for debugging
126 | *
127 | * @return String with internal state of the BusyBee instance.
128 | */
129 | fun toStringVerbose(): String
130 | fun interface NoLongerBusyCallback {
131 | /**
132 | * Called when there are no more operations in progress (that we are paying attention to)
133 | */
134 | fun noLongerBusy()
135 | }
136 |
137 | fun interface OperationMatcher {
138 | fun matches(o: Any): Boolean
139 | }
140 |
141 | enum class Category {
142 | GENERAL, NETWORK, DIALOG;
143 |
144 | companion object {
145 | fun defaultCategory(): Category {
146 | return GENERAL
147 | }
148 | }
149 | }
150 |
151 | companion object {
152 | /**
153 | * Generally, you want just one instance of BusyBee for your whole process.
154 | *
155 | *
156 | * For release apps with no tests running, this will return a instance of BusyBee
157 | * that does nothing, so there is minimal overhead for your release builds.
158 | *
159 | * @return the single global instance of BusyBee
160 | */
161 | fun singleton(): BusyBee {
162 | return BusyBeeSingleton.singleton()
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/busybee-core/src/main/java/io/americanexpress/busybee/BusyBeeExecutorWrapper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee
15 |
16 | import io.americanexpress.busybee.BusyBee.Category.Companion.defaultCategory
17 | import io.americanexpress.busybee.internal.NoOpBusyBee
18 | import java.util.concurrent.Executor
19 | import java.util.logging.Logger
20 |
21 | /**
22 | * This is an implementation of the Executor interface that will track operations
23 | * using the BusyBee. I.e. when this executor is executing something, BusyBee
24 | * will be "busyWith" all operations in progress that are submitted to this executor.
25 | *
26 | *
27 | * All executed runnables will be execute using the wrapped executor.
28 | */
29 | class BusyBeeExecutorWrapper private constructor(
30 | private val busyBee: BusyBee,
31 | private val category: BusyBee.Category,
32 | private val delegate: Executor
33 | ) : Executor {
34 | override fun execute(command: Runnable) {
35 | log.info("Starting $command on thread ${Thread.currentThread()}")
36 | busyBee.busyWith(command, category)
37 | delegate.execute {
38 | try {
39 | command.run()
40 | } finally {
41 | busyBee.completed(command)
42 | }
43 | }
44 | }
45 |
46 | class Builder {
47 | private lateinit var busyBee: BusyBee
48 | private var category: BusyBee.Category = defaultCategory()
49 | private var wrappedExecutor: Executor? = null
50 | fun busyBee(busyBee: BusyBee): Builder {
51 | this.busyBee = busyBee
52 | return this
53 | }
54 |
55 | fun executeInCategory(category: BusyBee.Category): Builder {
56 | this.category = category
57 | return this
58 | }
59 |
60 | fun wrapExecutor(delegate: Executor): Builder {
61 | wrappedExecutor = delegate
62 | return this
63 | }
64 |
65 | fun build(): Executor {
66 | val wrappedExecutorLocal = wrappedExecutor
67 | ?: throw NullPointerException("BusyBeeExecutorWrapper must have an underlying executor to wrap, can't be null.")
68 | return if (busyBee is NoOpBusyBee) {
69 | wrappedExecutorLocal
70 | } else {
71 | BusyBeeExecutorWrapper(busyBee, category, wrappedExecutorLocal)
72 | }
73 | }
74 | }
75 |
76 | companion object {
77 | private val log = Logger.getLogger("io.americanexpress.busybee")
78 | fun with(busyBee: BusyBee): Builder = Builder().busyBee(busyBee)
79 | }
80 | }
--------------------------------------------------------------------------------
/busybee-core/src/main/java/io/americanexpress/busybee/internal/BusyBeeSingleton.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.internal
15 |
16 | import androidx.annotation.VisibleForTesting
17 | import io.americanexpress.busybee.BusyBee
18 |
19 | object BusyBeeSingleton {
20 | private val SINGLETON by lazy { create() }
21 | fun singleton(): BusyBee = SINGLETON
22 |
23 | @VisibleForTesting
24 | fun create(): BusyBee {
25 | return if (EnvironmentChecks.testsAreRunning()) {
26 | RealBusyBee(MainThread.singletonExecutor)
27 | } else {
28 | NoOpBusyBee()
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/busybee-core/src/main/java/io/americanexpress/busybee/internal/EnvironmentChecks.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.internal
15 |
16 | import androidx.annotation.VisibleForTesting
17 | import io.americanexpress.busybee.internal.Reflection.classIsFound
18 | import io.americanexpress.busybee.internal.Reflection.clazz
19 | import io.americanexpress.busybee.internal.Reflection.invokeConstructor
20 | import io.americanexpress.busybee.internal.Reflection.invokeMethod
21 | import io.americanexpress.busybee.internal.Reflection.invokeStaticMethod
22 | import java.util.concurrent.ExecutionException
23 | import java.util.concurrent.FutureTask
24 | import java.util.concurrent.TimeUnit
25 | import java.util.concurrent.TimeoutException
26 |
27 | object EnvironmentChecks {
28 | @VisibleForTesting
29 | private var pretendTestsAreNotRunning = false
30 | fun testsAreRunning(): Boolean {
31 | return if (pretendTestsAreNotRunning) false else junit4IsPresent() || androidJunitRunnerIsPresent()
32 |
33 | // may want to add TestNG or Junit5 support at some point.
34 | }
35 |
36 | @VisibleForTesting
37 | fun junit4IsPresent(): Boolean = classIsFound("org.junit.runners.JUnit4")
38 |
39 | @VisibleForTesting
40 | fun androidJunitRunnerIsPresent(): Boolean = classIsFound("androidx.test.runner.AndroidJUnitRunner")
41 |
42 | fun hasWorkingAndroidMainLooper(): Boolean {
43 | val runnable: FutureTask
44 | try {
45 | val looperClass: Class<*> = clazz("android.os.Looper")
46 |
47 | val mainLooper = invokeStaticMethod(looperClass, "getMainLooper")
48 | val myLooper = invokeStaticMethod(looperClass, "myLooper")
49 | if (mainLooper == myLooper) {
50 | // we are already on the main looper so it must be working.
51 | return true
52 | }
53 | // else we will try and execute something on the main thread.
54 | val handlerClass: Class<*> = clazz("android.os.Handler")
55 | val handler = invokeConstructor(handlerClass, looperClass, mainLooper)
56 | runnable = FutureTask { true }
57 | invokeMethod(
58 | handler,
59 | "postAtFrontOfQueue",
60 | arrayOf(Runnable::class.java),
61 | arrayOf(runnable)
62 | )
63 | } catch (e: RuntimeException) {
64 | if (e.cause is ReflectiveOperationException) {
65 | // something that we needed doesn't exist, so Android Main Looper won't work
66 | return false
67 | }
68 | throw e
69 | }
70 | return try {
71 | runnable.get(5, TimeUnit.SECONDS)
72 | } catch (e: InterruptedException) {
73 | false
74 | } catch (e: ExecutionException) {
75 | false
76 | } catch (e: TimeoutException) {
77 | false
78 | }
79 | }
80 |
81 | @VisibleForTesting
82 | fun pretendTestsAreNotRunning() {
83 | pretendTestsAreNotRunning = true
84 | }
85 |
86 | @VisibleForTesting
87 | fun doNotPretendTestsAreNotRunning() {
88 | pretendTestsAreNotRunning = false
89 | }
90 | }
--------------------------------------------------------------------------------
/busybee-core/src/main/java/io/americanexpress/busybee/internal/MainThread.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.internal
15 |
16 | import io.americanexpress.busybee.internal.EnvironmentChecks.hasWorkingAndroidMainLooper
17 | import io.americanexpress.busybee.internal.Reflection.clazz
18 | import io.americanexpress.busybee.internal.Reflection.getField
19 | import io.americanexpress.busybee.internal.Reflection.getValue
20 | import java.util.concurrent.Executor
21 | import java.util.concurrent.Executors.newSingleThreadExecutor
22 |
23 | object MainThread {
24 | val singletonExecutor: Executor by lazy {
25 | /*
26 | * Can only load AndroidMainThreadExecutor if we are on Android (not JVM)
27 | * else will we get class not found errors.
28 | */
29 | return@lazy if (hasWorkingAndroidMainLooper()) {
30 | val androidExecutorClass = clazz(
31 | className = "io.americanexpress.busybee.android.internal.AndroidMainThreadExecutor",
32 | notFoundErrorMessage = "Must add busybee-android dependency when running on Android"
33 | )
34 | val instance = getField(androidExecutorClass, "INSTANCE")
35 | getValue(instance) as Executor
36 | } else {
37 | // use this on JVM when there is no Android Main Thread
38 | newSingleThreadExecutor()
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/busybee-core/src/main/java/io/americanexpress/busybee/internal/NoOpBusyBee.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.internal
15 |
16 | import io.americanexpress.busybee.BusyBee
17 | import io.americanexpress.busybee.BusyBee.NoLongerBusyCallback
18 | import io.americanexpress.busybee.BusyBee.OperationMatcher
19 |
20 | /**
21 | * This is a version of BusyBee that is used when the tests are not running.
22 | * It does nothing.
23 | * This minimizes the overhead of having BusyBee in your app.
24 | */
25 | class NoOpBusyBee internal constructor() : BusyBee {
26 | override fun getName(): String = "NO-OP BusyBee"
27 |
28 | override fun busyWith(operation: Any) {}
29 | override fun busyWith(operation: Any, category: BusyBee.Category) {}
30 | override fun registerNoLongerBusyCallback(noLongerBusyCallback: NoLongerBusyCallback) {}
31 | override fun payAttentionToCategory(category: BusyBee.Category) {}
32 | override fun ignoreCategory(category: BusyBee.Category) {}
33 | override fun completedEverythingInCategory(category: BusyBee.Category) {}
34 | override fun completedEverything() {}
35 | override fun completedEverythingMatching(matcher: OperationMatcher) {}
36 | override fun completed(operation: Any) {}
37 | override fun isNotBusy(): Boolean = true
38 | override fun isBusy(): Boolean = false
39 |
40 | override fun toStringVerbose(): String = "NO-OP BusyBee"
41 | }
--------------------------------------------------------------------------------
/busybee-core/src/main/java/io/americanexpress/busybee/internal/RealBusyBee.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.internal
15 |
16 | import androidx.annotation.GuardedBy
17 | import io.americanexpress.busybee.BusyBee
18 | import io.americanexpress.busybee.BusyBee.NoLongerBusyCallback
19 | import io.americanexpress.busybee.BusyBee.OperationMatcher
20 | import java.util.ArrayList
21 | import java.util.EnumSet
22 | import java.util.concurrent.Executor
23 | import java.util.concurrent.locks.Lock
24 | import java.util.concurrent.locks.ReentrantLock
25 | import java.util.logging.Logger
26 | import kotlin.concurrent.withLock
27 |
28 | /**
29 | * This allows the app to let Espresso (test framework) know when it is busy and Espresso should wait.
30 | * It is not directly tied to Espresso and could be used in any case where you need to know if the app is "busy".
31 | *
32 | *
33 | * Call busyWith when you start being "busy"
34 | * Call completed when you stop being "busy" and start being "idle".
35 | *
36 | *
37 | * Generally, you should call completed from a finally block.
38 | *
39 | *
40 | * Espresso will wait for the app to be "idle" (i.e. not busy).
41 | *
42 | *
43 | * Proper use of the BusyBee will avoid having to "wait" or "sleep" in tests.
44 | * BE SURE NOT BE "BUSY" LONGER THAN NECESSARY, otherwise it will slow down your tests.
45 | */
46 | class RealBusyBee(private val completedOnThread: Executor) : BusyBee {
47 | @GuardedBy("lock")
48 | private val operationsInProgress = SetMultiMap()
49 |
50 | @GuardedBy("lock")
51 | private val currentlyTrackedCategories = EnumSet.allOf(BusyBee.Category::class.java)
52 |
53 | @GuardedBy("lock")
54 | private val noLongerBusyCallbacks: MutableList =
55 | ArrayList(1) // Espresso use case will only have 1 callback
56 | private val lock: Lock = ReentrantLock()
57 | private val defaultCategory: BusyBee.Category = BusyBee.Category.defaultCategory()
58 | override fun getName(): String = lock.withLock {
59 | "${this.javaClass.simpleName}@${System.identityHashCode(this)} with operations: $operationsInProgress"
60 | }
61 |
62 | override fun busyWith(operation: Any) {
63 | busyWith(operation, defaultCategory)
64 | }
65 |
66 | override fun busyWith(operation: Any, category: BusyBee.Category) {
67 | lock.withLock {
68 | if (operationsInProgress.add(category, operation)) {
69 | log.info("busyWith -> [$operation] was added to active operations in category $category")
70 | }
71 | }
72 | }
73 |
74 | override fun registerNoLongerBusyCallback(noLongerBusyCallback: NoLongerBusyCallback) {
75 | lock.withLock {
76 | noLongerBusyCallbacks.add(noLongerBusyCallback)
77 | }
78 | }
79 |
80 | override fun payAttentionToCategory(category: BusyBee.Category) {
81 | lock.withLock {
82 | log.info("Paying attention to category: $category")
83 | currentlyTrackedCategories.add(category)
84 | }
85 | }
86 |
87 | override fun ignoreCategory(category: BusyBee.Category) {
88 | lock.withLock {
89 | log.info("Ignoring category: $category")
90 | val wasBusyBefore = isBusy()
91 | val wasRemoved = currentlyTrackedCategories.remove(category)
92 | val notBusyNow = isNotBusy()
93 | if (wasRemoved && wasBusyBefore && notBusyNow) {
94 | notifyNoLongerBusyCallbacks()
95 | }
96 | }
97 | }
98 |
99 | override fun completedEverythingInCategory(category: BusyBee.Category) {
100 | completedOnThread.execute(object : Runnable {
101 | override fun run() {
102 | lock.withLock {
103 | val iterator = operationsInProgress.valuesIterator(category)
104 | while (iterator.hasNext()) {
105 | val next = iterator.next()
106 | completeOnCurrentThread(next, iterator)
107 | }
108 | }
109 | }
110 |
111 | override fun toString() = "completedEverythingInCategory($category)"
112 | })
113 | }
114 |
115 | override fun completedEverything() {
116 | completedOnThread.execute(object : Runnable {
117 | override fun run() {
118 | lock.withLock {
119 | val iterator = operationsInProgress.valuesIterator()
120 | while (iterator.hasNext()) {
121 | val next = iterator.next()
122 | completeOnCurrentThread(next, iterator)
123 | }
124 | }
125 | }
126 |
127 | override fun toString() = "completedEverything()"
128 | })
129 | }
130 |
131 | override fun completedEverythingMatching(matcher: OperationMatcher) {
132 | completedOnThread.execute(object : Runnable {
133 | override fun run() {
134 | lock.withLock {
135 | val iterator = operationsInProgress.valuesIterator()
136 | while (iterator.hasNext()) {
137 | val next = iterator.next()
138 | if (matcher.matches(next)) {
139 | completeOnCurrentThread(next, iterator)
140 | }
141 | }
142 | }
143 | }
144 |
145 | override fun toString() = "completedEverythingMatching($matcher)"
146 | })
147 | }
148 |
149 | override fun completed(operation: Any) {
150 | completedOnThread.execute(object : Runnable {
151 | override fun run() {
152 | completeOnCurrentThread(operation, null)
153 | }
154 |
155 | override fun toString() = "completed($operation)"
156 | })
157 | }
158 |
159 | /**
160 | * Precondition: Iterator must be pointing to the operation passed in (or iterator must be null).
161 | *
162 | *
163 | * This method "completes" the operation on the current thread.
164 | *
165 | * @param operation the operation to be completed
166 | * @param iterator must be pointing to operation
167 | */
168 | private fun completeOnCurrentThread(operation: Any, iterator: MutableIterator?) {
169 | lock.withLock {
170 | val wasRemoved = if (iterator != null) {
171 | // if the collection is being iterated,
172 | // then we HAVE to use the iterator for removal to avoid ConcurrentModificationException
173 | iterator.remove()
174 | true
175 | } else {
176 | operationsInProgress.removeValue(operation)
177 | }
178 | if (wasRemoved) {
179 | log.info("completed -> [$operation] was removed from active operations")
180 | }
181 | if (wasRemoved && isNotBusy()) {
182 | notifyNoLongerBusyCallbacks()
183 | }
184 | }
185 | }
186 |
187 | private fun notifyNoLongerBusyCallbacks() {
188 | for (noLongerBusyCallback in noLongerBusyCallbacks) {
189 | log.info("All operations are now finished, we are now idle")
190 | noLongerBusyCallback.noLongerBusy()
191 | }
192 | }
193 |
194 | override fun isNotBusy(): Boolean {
195 | lock.withLock {
196 | for (category in currentlyTrackedCategories) {
197 | if (isBusyWithAnythingIn(category)) return false
198 | }
199 | return true
200 | }
201 | }
202 |
203 | override fun isBusy(): Boolean = !isNotBusy()
204 |
205 | private fun isBusyWithAnythingIn(category: BusyBee.Category) = lock.withLock {
206 | operationsInProgress.values(category).isNotEmpty()
207 | }
208 |
209 | override fun toStringVerbose(): String = lock.withLock {
210 | val operations = operationsInProgress
211 | buildString {
212 | appendLine(
213 | """
214 | ***********************
215 | **BusyBee Information**
216 | ***********************
217 | """.trimIndent()
218 | )
219 | try {
220 | appendLine(
221 | """
222 | Total Operations: ${operations.allValues().size}
223 | List of operations in progress:
224 | ****************************
225 | """.trimIndent()
226 | )
227 | for (category in operations.allKeys()) {
228 | appendLine(
229 | """
230 | CATEGORY: ======= ${category.name} =======
231 | """.trimIndent()
232 | )
233 | for (operation in operations.values(category)) {
234 | appendLine(operation)
235 | }
236 | }
237 | } catch (e: Exception) {
238 | appendLine(
239 | """
240 | ${e.message}
241 | ****!!!!FAILED TO GET LIST OF IN PROGRESS OPERATIONS!!!!****
242 | """.trimIndent()
243 | )
244 | }
245 | appendLine("****************************")
246 | }
247 | }
248 |
249 | companion object {
250 | private val log = Logger.getLogger("io.americanexpress.busybee")
251 | }
252 | }
--------------------------------------------------------------------------------
/busybee-core/src/main/java/io/americanexpress/busybee/internal/Reflection.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.internal
15 |
16 | import java.lang.reflect.Field
17 | import java.lang.reflect.InvocationTargetException
18 |
19 | // we are doing a number of type-unsafe things here to seamlessly detect whether we are on Android or not.
20 | // Can't use the actual Android types because they might not exist, if we are in a pure JVM module
21 | internal object Reflection {
22 | fun getValue(instance: Field): Any = try {
23 | instance.get(null)
24 | } catch (e: IllegalAccessException) {
25 | throw RuntimeException(e)
26 | }
27 |
28 | fun getField(clazz: Class<*>, fieldName: String): Field = try {
29 | clazz.getDeclaredField(fieldName)
30 | } catch (e: NoSuchFieldException) {
31 | throw RuntimeException(e)
32 | }
33 |
34 | fun invokeStaticMethod(clazz: Class<*>, methodName: String): Any = try {
35 | clazz.getMethod(methodName).invoke(null)
36 | } catch (e: NoSuchMethodException) {
37 | throw RuntimeException(e)
38 | } catch (e: IllegalAccessException) {
39 | throw RuntimeException(e)
40 | } catch (e: InvocationTargetException) {
41 | throw RuntimeException(e)
42 | }
43 |
44 | fun invokeMethod(
45 | instance: Any,
46 | methodName: String,
47 | argTypes: Array>,
48 | args: Array,
49 | ): Any = try {
50 | instance.javaClass.getMethod(methodName, *argTypes).invoke(instance, *args)
51 | } catch (e: NoSuchMethodException) {
52 | throw RuntimeException(e)
53 | } catch (e: IllegalAccessException) {
54 | throw RuntimeException(e)
55 | } catch (e: InvocationTargetException) {
56 | throw RuntimeException(e)
57 | }
58 |
59 | @JvmOverloads
60 | fun clazz(
61 | className: String,
62 | notFoundErrorMessage: String = "Error calling Class.forName on $className",
63 | ): Class<*> = try {
64 | Class.forName(className)
65 | } catch (e: ClassNotFoundException) {
66 | throw RuntimeException(notFoundErrorMessage, e)
67 | }
68 |
69 | fun classIsFound(className: String): Boolean {
70 | try {
71 | Class.forName(className)
72 | } catch (e: ClassNotFoundException) {
73 | return false
74 | }
75 | return true
76 | }
77 |
78 | fun invokeConstructor(
79 | classToConstruct: Class<*>,
80 | constructorArgType: Class<*>,
81 | constructorArg: Any,
82 | ): Any = try {
83 | classToConstruct.getConstructor(constructorArgType).newInstance(constructorArg)
84 | } catch (e: NoSuchMethodException) {
85 | throw RuntimeException(e)
86 | } catch (e: InstantiationException) {
87 | throw RuntimeException(e)
88 | } catch (e: IllegalAccessException) {
89 | throw RuntimeException(e)
90 | } catch (e: InvocationTargetException) {
91 | throw RuntimeException(e)
92 | }
93 | }
--------------------------------------------------------------------------------
/busybee-core/src/main/java/io/americanexpress/busybee/internal/SetMultiMap.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.internal
15 |
16 | /**
17 | * Simple purpose-built SetMultiMap designed for the needs of BusyBee.
18 | * We didn't want to have BusyBee depend on guava, so Guava's SetMultiMap wasn't an option.
19 | * Builds on top of mutableMap/mutableSet internally.
20 | *
21 | *
22 | * This collection maps keys of type K to sets of values of type V.
23 | * Each value is unique across all keys in the collection.
24 | *
25 | *
26 | * Not the most efficient impl, but good enough for the BusyBee use case.
27 | *
28 | *
29 | * WARNING: this Impl has some weird quirks, so probably not good for general purpose use.
30 | * Biggest quirk is the at each value is unique across all keys.
31 | * if you want to change the key for a value, you must remove it and re-add it.
32 | *
33 | *
34 | * See the method JavaDocs for details.
35 | */
36 | class SetMultiMap {
37 | private val map = mutableMapOf>()
38 | private val reverseMap = mutableMapOf()
39 |
40 | /**
41 | * All keys are unique and all values are unique.
42 | * If the Value already exists under any Key,
43 | * then it will not be added again.
44 | *
45 | *
46 | * WARNING:
47 | * To re-associate with another Key, first `removeValue` then re-add the Value
48 | * Else this method will throw an exception.
49 | *
50 | * @return true if a new entry was added.
51 | * @throws IllegalStateException when you add the SAME value again with a DIFFERENT key.
52 | */
53 | @Throws(IllegalStateException::class)
54 | fun add(key: K, value: V): Boolean {
55 | check(!(reverseMap[value] != null && reverseMap[value] !== key)) {
56 | """
57 | You can't insert the same value for 2 different keys.
58 | This mapping already exists:
59 | '${reverseMap[value]}' =>
60 | '$value'
61 | but you tried to add this new mapping:
62 | '$key' =>
63 | '$value'
64 | Remove the old mapping first!
65 | """.trimIndent()
66 | }
67 | if (reverseMap[value] === key) {
68 | return false
69 | }
70 | var valueSet = map[key]
71 | if (valueSet == null) {
72 | valueSet = mutableSetOf()
73 | map[key] = valueSet
74 | }
75 | valueSet.add(value)
76 | reverseMap[value] = key
77 | return true
78 | }
79 |
80 | /**
81 | * Each value ONLY appears once, so it is associated with at most one key.
82 | *
83 | * @return key for which the value is associated with.
84 | */
85 | fun keyFor(value: V): K? {
86 | return reverseMap[value]
87 | }
88 |
89 | /**
90 | * Removes the value if and only if it exists in the map.
91 | *
92 | * @return true if an entry was removed.
93 | */
94 | fun removeValue(value: V): Boolean {
95 | if (reverseMap.containsKey(value)) {
96 | val keyForRemoved = reverseMap.remove(value)
97 | ?: throw IllegalStateException("Value not in reverseMap: $value")
98 | (map[keyForRemoved]
99 | ?: throw IllegalStateException("Value removed from reverseMap, but not in map: $value"))
100 | .remove(value)
101 | return true
102 | }
103 | return false
104 | }
105 |
106 | /**
107 | * @return provides an iterator ( with remove support ) for all the values in the collection.
108 | */
109 | fun valuesIterator(): MutableIterator {
110 | val valueToKeyEntryIterator: MutableIterator> = reverseMap.entries.iterator()
111 | return multiMapIteratorFromReverseMapIterator(valueToKeyEntryIterator)
112 | }
113 |
114 | private fun multiMapIteratorFromReverseMapIterator(reverseMapIterator: MutableIterator>): MutableIterator {
115 | return object : MutableIterator {
116 | private lateinit var reverseEntry: Map.Entry
117 | override fun hasNext(): Boolean {
118 | return reverseMapIterator.hasNext()
119 | }
120 |
121 | override fun next(): V {
122 | reverseEntry = reverseMapIterator.next()
123 | return reverseEntry.key
124 | }
125 |
126 | override fun remove() {
127 | reverseMapIterator.remove()
128 | val setOfValuesFromForwardMap =
129 | requireNotNull(map[reverseEntry.value]) { "Invariant not met. Key not found in forward map." }
130 | setOfValuesFromForwardMap.remove(reverseEntry.key)
131 | }
132 | }
133 | }
134 |
135 | private fun multiMapIteratorFromForwardMapIterator(valueIterator: MutableIterator): MutableIterator {
136 | return object : MutableIterator {
137 | private lateinit var nextValue: V
138 | override fun hasNext(): Boolean {
139 | return valueIterator.hasNext()
140 | }
141 |
142 | override fun next(): V {
143 | nextValue = valueIterator.next()
144 | return nextValue
145 | }
146 |
147 | override fun remove() {
148 | valueIterator.remove()
149 | reverseMap.remove(nextValue)
150 | }
151 | }
152 | }
153 |
154 | /**
155 | * @return All the keys that has ever been used in this map since its creation
156 | */
157 | fun allKeys(): Set {
158 | return map.keys.toSet()
159 | }
160 |
161 | /**
162 | * @return All values across all keys
163 | */
164 | fun allValues(): Set {
165 | return reverseMap.keys.toSet()
166 | }
167 |
168 | /**
169 | * @return Values for the given key
170 | */
171 | fun values(key: K): Set = map[key]?.toSet() ?: emptySet()
172 |
173 | /**
174 | * @return True if and only if there are no values ( there may be keys )
175 | */
176 | fun hasNoValues(): Boolean = reverseMap.isEmpty()
177 |
178 | fun valuesIterator(key: K): MutableIterator {
179 | val values: MutableSet = map[key] ?: mutableSetOf()
180 | val valueIterator = values.iterator()
181 | return multiMapIteratorFromForwardMapIterator(valueIterator)
182 | }
183 |
184 | override fun toString(): String {
185 | val sb = StringBuilder()
186 | sb.append("\n{\n")
187 | for ((key, value1) in map) {
188 | sb.append("'").append(key).append("'\n")
189 | val iterator = value1.iterator()
190 | while (iterator.hasNext()) {
191 | val value = iterator.next()
192 | if (iterator.hasNext()) {
193 | sb.append(" ├─ '")
194 | } else {
195 | sb.append(" └─ '")
196 | }
197 | sb.append(value).append("'\n")
198 | }
199 | }
200 | sb.append("}")
201 | return sb.toString()
202 | }
203 | }
--------------------------------------------------------------------------------
/busybee-core/src/test/java/io/americanexpress/busybee/BusyBeeTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee
15 |
16 | import io.americanexpress.busybee.BusyBee.Category.NETWORK
17 | import io.americanexpress.busybee.BusyBee.NoLongerBusyCallback
18 | import io.americanexpress.busybee.internal.RealBusyBee
19 | import org.assertj.core.api.Java6Assertions.assertThat
20 | import org.junit.Assert.assertFalse
21 | import org.junit.Assert.assertTrue
22 | import org.junit.Assert.fail
23 | import org.junit.Before
24 | import org.junit.Test
25 | import org.mockito.Mockito.mock
26 | import org.mockito.Mockito.verify
27 | import java.util.concurrent.Executor
28 |
29 | class BusyBeeTest {
30 | private lateinit var busyBee: BusyBee
31 |
32 | @Before
33 | fun setUp() {
34 | busyBee = RealBusyBee(IMMEDIATE)
35 | }
36 |
37 | @Test
38 | fun whenBusy_thenIsNotBusyReturnsFalse() {
39 | busyBee.busyWith(this)
40 |
41 | assertIsBusy(busyBee)
42 | }
43 |
44 | @Test
45 | fun whenNetworkBusyAndNotTrackingNetwork_thenIsNotBusy() {
46 | busyBee.busyWith(this, NETWORK)
47 | busyBee.ignoreCategory(NETWORK)
48 |
49 | // should not be busy because we aren't tracking network requests
50 | assertNotBusy(busyBee)
51 | }
52 |
53 | @Test
54 | fun whenNetworkBusyAndPayAttention_thenIsBusy() {
55 | busyBee.busyWith(this, NETWORK)
56 | busyBee.ignoreCategory(NETWORK)
57 | busyBee.payAttentionToCategory(NETWORK)
58 |
59 | assertIsBusy(busyBee)
60 | }
61 |
62 | @Test
63 | fun whenNetworkBusyAndCompletedNetwork_thenIsNotBusy() {
64 | busyBee.busyWith(this, NETWORK)
65 | busyBee.completedEverythingInCategory(NETWORK)
66 |
67 | assertNotBusy(busyBee)
68 | }
69 |
70 | @Test
71 | fun whenCompletedNetwork_thenIsNotBusy() {
72 | busyBee.completedEverythingInCategory(NETWORK)
73 |
74 | assertNotBusy(busyBee)
75 | }
76 |
77 | @Test
78 | fun whenNetworkBusy_thenIsNotBusyReturnsFalse() {
79 | busyBee.busyWith(this, NETWORK)
80 |
81 | assertIsBusy(busyBee)
82 | }
83 |
84 | @Test
85 | fun whenCompleted_thenIsNotBusyReturnsTrue() {
86 | busyBee.busyWith(this)
87 | busyBee.completed(this)
88 |
89 | assertNotBusy(busyBee)
90 | }
91 |
92 | // so we can pass in null as if we are calling from Java
93 | private fun nullForTesting(): Any = null!!
94 |
95 | @Test
96 | fun whenBusyWithNull_thenThrowNullPointerException() {
97 | try {
98 | busyBee.busyWith(nullForTesting())
99 | fail("busyWith(null) should throw NullPointerException")
100 | } catch (e: NullPointerException) {
101 | // expected
102 | }
103 | }
104 |
105 | @Test
106 | fun whenCompletedNull_thenThrowNullPointerException() {
107 | try {
108 | busyBee.completed(nullForTesting())
109 | fail("completed(null) should throw NullPointerException")
110 | } catch (e: NullPointerException) {
111 | // expected
112 | }
113 | }
114 |
115 | @Test
116 | fun whenBusyWithSomething_thenNameIncludesSomething() {
117 | busyBee.busyWith("some thing")
118 |
119 | assertThat(busyBee.getName()).contains("some thing")
120 | }
121 |
122 | @Test
123 | fun whenCompletedEverything_thenIsNotBusyReturnsTrue() {
124 | busyBee.busyWith(this)
125 | busyBee.busyWith(Any())
126 | busyBee.busyWith(Any())
127 | busyBee.completedEverything()
128 |
129 | assertNotBusy(busyBee)
130 | }
131 |
132 | @Test
133 | fun whenCompleteString_thenOnlyNonStringOperationsRemain() {
134 | val operation = Any()
135 | busyBee.busyWith(operation)
136 | busyBee.busyWith("Network 1", NETWORK)
137 | busyBee.busyWith("General")
138 | busyBee.completedEverythingMatching { o: Any? -> o is String }
139 |
140 | // still busy with 1 non-String operation
141 | assertIsBusy(busyBee)
142 |
143 | busyBee.completed(operation)
144 |
145 | assertNotBusy(busyBee)
146 | }
147 |
148 | @Test
149 | fun whenCompleted_thenNotifyCallback() {
150 | busyBee.busyWith("Op1")
151 | val noLongerBusyCallback = mock(NoLongerBusyCallback::class.java)
152 | busyBee.registerNoLongerBusyCallback(noLongerBusyCallback)
153 | busyBee.completed("Op1")
154 |
155 | verify(noLongerBusyCallback).noLongerBusy()
156 | }
157 |
158 | @Test
159 | fun whenIgnoreRemainingOperations_thenNotifyCallback() {
160 | busyBee.payAttentionToCategory(NETWORK)
161 | busyBee.busyWith("Op1")
162 | busyBee.busyWith("Network", NETWORK)
163 | val noLongerBusyCallback = mock(NoLongerBusyCallback::class.java)
164 | busyBee.registerNoLongerBusyCallback(noLongerBusyCallback)
165 | busyBee.completed("Op1")
166 | busyBee.ignoreCategory(NETWORK)
167 |
168 | verify(noLongerBusyCallback).noLongerBusy()
169 | }
170 |
171 | @Test
172 | fun whenCompletedOnlySome_thenStillBusy() {
173 | val o1 = Any()
174 | val o2 = Any()
175 | assertNotBusy(busyBee)
176 |
177 | busyBee.busyWith(o1)
178 | assertIsBusy(busyBee)
179 |
180 | busyBee.busyWith(o2)
181 | assertIsBusy(busyBee)
182 |
183 | busyBee.completed(o2)
184 | assertIsBusy(busyBee)
185 |
186 | busyBee.completed(o1)
187 | assertNotBusy(busyBee)
188 | }
189 |
190 | companion object {
191 | private val IMMEDIATE: Executor = Executor { obj: Runnable -> obj.run() }
192 |
193 | private fun assertNotBusy(busyBee: BusyBee) {
194 | assertTrue(busyBee.isNotBusy())
195 | }
196 |
197 | private fun assertIsBusy(busyBee: BusyBee) {
198 | assertFalse(busyBee.isNotBusy())
199 | }
200 | }
201 | }
--------------------------------------------------------------------------------
/busybee-core/src/test/java/io/americanexpress/busybee/internal/BusyBeeExecutorWrapperTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.internal
15 |
16 | import io.americanexpress.busybee.BusyBee.Category.GENERAL
17 | import io.americanexpress.busybee.BusyBee.Category.NETWORK
18 | import io.americanexpress.busybee.BusyBeeExecutorWrapper
19 | import org.assertj.core.api.Java6Assertions.assertThat
20 | import org.junit.Test
21 | import java.util.concurrent.CountDownLatch
22 | import java.util.concurrent.Executor
23 | import java.util.concurrent.ExecutorService
24 | import java.util.concurrent.Executors
25 | import java.util.concurrent.TimeUnit.SECONDS
26 |
27 | class BusyBeeExecutorWrapperTest {
28 | @Test
29 | fun whenExecutorWrappedForNoOp_thenItDoesNotGetWrapped() {
30 | val originalExecutor = Executor { obj: Runnable -> obj.run() }
31 | val wrappedExecutor =
32 | BusyBeeExecutorWrapper.with(NoOpBusyBee())
33 | .wrapExecutor(originalExecutor)
34 | .build()
35 | assertThat(wrappedExecutor === originalExecutor)
36 | .`as`("Expected wrapped executor to equal original executor for NoOpBusyBee")
37 | .isTrue()
38 | }
39 |
40 | @Test
41 | fun whenWrappedWithBusyBeeExecutorWrapper_thenBusyBeeSaysItIsBusy() {
42 | val mainThread = MainThread.singletonExecutor as ExecutorService
43 | val busyBee = RealBusyBee(mainThread)
44 | val wrappedThread = Executors.newSingleThreadExecutor()
45 | val executorWrapper =
46 | BusyBeeExecutorWrapper.with(busyBee)
47 | .executeInCategory(NETWORK)
48 | .wrapExecutor(wrappedThread)
49 | .build()
50 |
51 | val busyLatch = CountDownLatch(1)
52 | executorWrapper.execute {
53 | try {
54 | busyLatch.await()
55 | } catch (e: InterruptedException) {
56 | throw RuntimeException(e)
57 | }
58 | }
59 |
60 | // shouldn't complete anything because our executor uses NETWORK category
61 | busyBee.completedEverythingInCategory(GENERAL)
62 |
63 | assertThat(busyBee.isBusy())
64 | .`as`("Should be \"busy\" waiting for Latch")
65 | .isTrue()
66 | assertThat(busyBee.isNotBusy())
67 | .`as`("Should be \"busy\" waiting for Latch")
68 | .isFalse()
69 |
70 | busyLatch.countDown()
71 |
72 | // wrappedThread will post a Runnable to MainThread to tell BusyBee it completed.
73 | wrappedThread.shutdown()
74 | wrappedThread.awaitTermination(10, SECONDS)
75 |
76 | // wait for the completion message from the wrapped thread to run on the main thread.
77 | mainThread.shutdown()
78 | mainThread.awaitTermination(10, SECONDS)
79 |
80 | assertThat(busyBee.isBusy())
81 | .`as`("Should no longer be \"busy\" waiting for Latch")
82 | .isFalse()
83 | assertThat(busyBee.isNotBusy())
84 | .`as`("Should no longer be \"busy\" waiting for Latch")
85 | .isTrue()
86 | }
87 | }
--------------------------------------------------------------------------------
/busybee-core/src/test/java/io/americanexpress/busybee/internal/BusyBeeSingletonTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 |
15 | package io.americanexpress.busybee.internal
16 |
17 | import io.americanexpress.busybee.internal.EnvironmentChecks.androidJunitRunnerIsPresent
18 | import io.americanexpress.busybee.internal.EnvironmentChecks.doNotPretendTestsAreNotRunning
19 | import io.americanexpress.busybee.internal.EnvironmentChecks.pretendTestsAreNotRunning
20 | import org.assertj.core.api.Java6Assertions.assertThat
21 | import org.junit.Test
22 |
23 | class BusyBeeSingletonTest {
24 | @Test
25 | fun whenInJvmTest_espressoTestsAreNotRunning() {
26 | assertThat(androidJunitRunnerIsPresent()).isFalse()
27 | }
28 |
29 | @Test
30 | fun whenInJvmTest_useRealBusyBee() {
31 | assertThat(BusyBeeSingleton.create()).isInstanceOf(RealBusyBee::class.java)
32 | }
33 |
34 | @Test
35 | fun whenTestsAreNotRunning_useNoOpBusyBee() {
36 | pretendTestsAreNotRunning()
37 | assertThat(BusyBeeSingleton.create()).isInstanceOf(NoOpBusyBee::class.java)
38 | doNotPretendTestsAreNotRunning()
39 | }
40 | }
--------------------------------------------------------------------------------
/busybee-core/src/test/java/io/americanexpress/busybee/internal/EnvironmentChecksTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 |
15 | package io.americanexpress.busybee.internal
16 |
17 | import io.americanexpress.busybee.internal.EnvironmentChecks.androidJunitRunnerIsPresent
18 | import io.americanexpress.busybee.internal.EnvironmentChecks.hasWorkingAndroidMainLooper
19 | import io.americanexpress.busybee.internal.EnvironmentChecks.testsAreRunning
20 | import org.assertj.core.api.Java6Assertions.assertThat
21 | import org.junit.Test
22 |
23 | class EnvironmentChecksTest {
24 | @Test
25 | fun testsAreRunningShouldAlwaysBeTrueInATest() {
26 | assertThat(testsAreRunning()).isTrue()
27 | }
28 |
29 | @Test
30 | fun whenInNonAndroidModule_thenEspressoIdlingResourceIsPresentIsFalse() {
31 | assertThat(androidJunitRunnerIsPresent()).isFalse()
32 | }
33 |
34 | @Test
35 | fun whenInNonAndroidModule_thenIsAndroidReturnsFalse() {
36 | assertThat(hasWorkingAndroidMainLooper()).isFalse()
37 | }
38 | }
--------------------------------------------------------------------------------
/busybee-core/src/test/java/io/americanexpress/busybee/internal/MainThreadTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 |
15 | package io.americanexpress.busybee.internal
16 |
17 | import org.assertj.core.api.Java6Assertions.assertThat
18 | import org.junit.Test
19 |
20 | class MainThreadTest {
21 | @Test
22 | fun whenOnJvm_thenWeGetTheNonAndroidExecutor() {
23 | val executor = MainThread.singletonExecutor
24 | assertThat(executor.javaClass.name)
25 | .isNotEqualTo("io.americanexpress.busybee.android.internal.AndroidMainThreadExecutor")
26 | }
27 | }
--------------------------------------------------------------------------------
/busybee-core/src/test/java/io/americanexpress/busybee/internal/NoOpBusyBeeTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.internal
15 |
16 | import io.americanexpress.busybee.BusyBee.Category.GENERAL
17 | import io.americanexpress.busybee.BusyBee.Category.NETWORK
18 | import org.assertj.core.api.Java6Assertions.assertThat
19 | import org.junit.Test
20 |
21 | class NoOpBusyBeeTest {
22 | @Test
23 | fun whenNoOpBusyWith_thenNotBusy() {
24 | val busyBee = NoOpBusyBee()
25 | busyBee.busyWith("SOMETHING")
26 |
27 | assertThat(busyBee.isNotBusy()).isTrue()
28 | assertThat(busyBee.isBusy()).isFalse()
29 | }
30 |
31 | @Test
32 | fun whenNoOpBusyWith_thenNothingThrowsException() {
33 | val busyBee = NoOpBusyBee()
34 | busyBee.busyWith("SOMETHING")
35 | busyBee.completed("SOMETHING")
36 | busyBee.payAttentionToCategory(NETWORK)
37 | busyBee.ignoreCategory(NETWORK)
38 | busyBee.completedEverything()
39 | busyBee.completedEverythingInCategory(GENERAL)
40 |
41 | assertThat(busyBee.isNotBusy()).isTrue()
42 | assertThat(busyBee.isBusy()).isFalse()
43 | assertThat(busyBee.getName()).isNotNull()
44 | assertThat(busyBee.toStringVerbose()).isNotNull()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/busybee-core/src/test/java/io/americanexpress/busybee/internal/ReflectionTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 |
15 | package io.americanexpress.busybee.internal
16 |
17 | import io.americanexpress.busybee.internal.Reflection.classIsFound
18 | import org.assertj.core.api.Java6Assertions.assertThat
19 | import org.junit.Test
20 |
21 | class ReflectionTest {
22 | @Test
23 | fun canFindThisClass() {
24 | assertThat(classIsFound("io.americanexpress.busybee.internal.ReflectionTest"))
25 | .`as`("must be able to find this class")
26 | .isTrue()
27 | }
28 |
29 | @Test
30 | fun cantFindNonExistentClass() {
31 | assertThat(classIsFound("com.doesnt.exist.Class"))
32 | .`as`("That class should not exist")
33 | .isFalse()
34 | }
35 | }
--------------------------------------------------------------------------------
/busybee-core/src/test/java/io/americanexpress/busybee/internal/SetMultiMapTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.internal
15 |
16 | import org.assertj.core.api.Java6Assertions.assertThat
17 | import org.assertj.core.api.Java6Assertions.fail
18 | import org.junit.Before
19 | import org.junit.Test
20 |
21 | class SetMultiMapTest {
22 | private lateinit var map: SetMultiMap
23 |
24 | @Before
25 | fun setUp() {
26 | map = SetMultiMap()
27 | }
28 |
29 | @Test
30 | fun whenValueAdd_thenCanBeRetrieved() {
31 | map.add(1, "1")
32 | map.add(1, "2")
33 | map.add(2, "4")
34 | map.add(2, "3")
35 |
36 | assertThat(map.values(1))
37 | .containsExactlyInAnyOrder("1", "2")
38 | assertThat(map.values(2))
39 | .containsExactlyInAnyOrder("4", "3")
40 | assertThat(map.allValues())
41 | .containsExactlyInAnyOrder("1", "2", "3", "4")
42 | }
43 |
44 | @Test
45 | fun whenValueAddedFirstTime_thenReturnTrue() {
46 | val wasAdded = map.add(1, "2")
47 |
48 | assertThat(wasAdded).isTrue()
49 | }
50 |
51 | @Test
52 | fun whenKeyValueAddedSecondTime_thenReturnFalse() {
53 | map.add(2, "2")
54 | val resultForSecondAdd = map.add(2, "2")
55 |
56 | assertThat(resultForSecondAdd).isFalse()
57 | }
58 |
59 | @Test
60 | fun whenNoValues_thenReturnEmptySet() {
61 | assertThat(map.values(1)).isEmpty()
62 | }
63 |
64 | @Test
65 | fun whenKeyFor_thenReturnValue() {
66 | map.add(1, "1")
67 | map.add(2, "3")
68 | map.add(1, "2")
69 |
70 | assertThat(map.keyFor("1")).isEqualTo(1)
71 | }
72 |
73 | @Test
74 | fun whenAddSameValue_thenThrowException() {
75 | try {
76 | map.add(1, "1")
77 | map.add(2, "1")
78 | fail("Adding the same value twice must throw an exception")
79 | } catch (e: IllegalStateException) {
80 | // expected
81 | }
82 | }
83 |
84 | @Test
85 | fun whenRemoveValue_thenCanAddBackWithDifferentKey() {
86 | map.add(1, "1")
87 | map.add(1, "3")
88 | map.removeValue("1")
89 | map.add(2, "1")
90 | map.add(1, "2")
91 |
92 | assertThat(map.keyFor("1")).isEqualTo(2)
93 | }
94 |
95 | @Test
96 | fun whenRemoveTwice_thenReturnFalseOnSecondTime() {
97 | map.add(1, "1")
98 | map.add(2, "2")
99 | val resultWhenValuePresent = map.removeValue("1")
100 | val resultAfterValueRemoved = map.removeValue("1")
101 |
102 | assertThat(resultWhenValuePresent).isTrue()
103 | assertThat(resultAfterValueRemoved).isFalse()
104 | }
105 |
106 | @Test
107 | fun whenAllKeys_thenAllKeysArePresent() {
108 | map.add(1, "1")
109 | map.add(2, "2")
110 | map.add(2, "3")
111 | assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2)
112 | }
113 |
114 | @Test
115 | fun whenAllValues_thenAllValuesArePresent() {
116 | map.add(1, "1")
117 | map.add(2, "2")
118 | map.add(2, "3")
119 |
120 | assertThat(map.allValues()).containsExactlyInAnyOrder("1", "2", "3")
121 | }
122 |
123 | @Test
124 | fun valuesReturnsOnlyValuesForTheSpecifiedKey() {
125 | map.add(1, "1")
126 | map.add(2, "2")
127 | map.add(2, "3")
128 |
129 | assertThat(map.values(2)).containsExactlyInAnyOrder("2", "3")
130 | }
131 |
132 | @Test
133 | fun whenAllValuesRemoved_ThenIsEmpty() {
134 | map.add(1, "1")
135 | map.removeValue("1")
136 |
137 | assertThat(map.hasNoValues()).isTrue()
138 | }
139 |
140 | @Test
141 | fun whenHasValue_ThenNotIsEmpty() {
142 | map.add(1, "1")
143 |
144 | assertThat(map.hasNoValues()).isFalse()
145 | }
146 |
147 | @Test
148 | fun whenIterateRemove_ThenIsActuallyRemoved() {
149 | map.add(1, "A")
150 | map.add(1, "B")
151 | map.add(2, "C")
152 | map.add(2, "C")
153 | map.add(3, "E")
154 | map.add(3, "D")
155 | val stringIterator = map.valuesIterator()
156 | while (stringIterator.hasNext()) {
157 | val next = stringIterator.next()
158 | if (next == "C") {
159 | stringIterator.remove()
160 | }
161 | }
162 |
163 | assertThat(map.allValues()).containsExactlyInAnyOrder("A", "B", "D", "E")
164 | assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2, 3)
165 | assertThat(map.values(2)).isEmpty()
166 | }
167 |
168 | @Test
169 | fun whenIterateValuesForKeyAndRemove_ThenIsActuallyRemoved() {
170 | map.add(1, "A")
171 | map.add(1, "B")
172 | map.add(2, "X")
173 | map.add(2, "Y")
174 | map.add(3, "E")
175 | map.add(3, "C")
176 | map.add(3, "D")
177 | val stringIterator = map.valuesIterator(3)
178 | while (stringIterator.hasNext()) {
179 | val next = stringIterator.next()
180 | if (next == "C") {
181 | stringIterator.remove()
182 | }
183 | }
184 | assertThat(map.allValues()).containsExactlyInAnyOrder("A", "B", "X", "E", "Y", "D")
185 | assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2, 3)
186 | assertThat(map.values(3)).containsExactlyInAnyOrder("D", "E")
187 | }
188 |
189 | @Test
190 | fun whenIterateValuesForKeyWithNoValues_thenNoExceptionThrown() {
191 | map.add(1, "A")
192 | map.add(1, "B")
193 | map.add(2, "X")
194 | map.add(2, "Y")
195 | map.add(3, "E")
196 | map.add(3, "C")
197 | map.add(3, "D")
198 | val stringIterator = map.valuesIterator(4)
199 | while (stringIterator.hasNext()) {
200 | val next = stringIterator.next()
201 | if (next == "C") {
202 | stringIterator.remove()
203 | }
204 | }
205 | assertThat(map.allValues()).containsExactlyInAnyOrder("A", "C", "B", "X", "E", "Y", "D")
206 | assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2, 3)
207 | assertThat(map.values(3)).containsExactlyInAnyOrder("D", "E", "C")
208 | }
209 |
210 | @Test
211 | fun toString_thenPrintSomethingReasonable() {
212 | map.add(1, "A")
213 | map.add(1, "B")
214 | map.add(2, "X")
215 | map.add(2, "Y")
216 | map.add(3, "E")
217 | map.add(3, "C")
218 | map.add(3, "D")
219 |
220 | assertThat(map.toString()).isEqualTo(
221 | "\n" + """
222 | {
223 | '1'
224 | ├─ 'A'
225 | └─ 'B'
226 | '2'
227 | ├─ 'X'
228 | └─ 'Y'
229 | '3'
230 | ├─ 'E'
231 | ├─ 'C'
232 | └─ 'D'
233 | }""".trimIndent()
234 | )
235 |
236 | }
237 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2019 American Express Travel Related Services Company, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | # in compliance with the License. You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software distributed under the License
10 | # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | # or implied. See the License for the specific language governing permissions and limitations under
12 | # the License.
13 | #
14 | android.useAndroidX=true
15 | systemProp.group=io.americanexpress.busybee
16 | systemProp.version=0.0.5
17 | kotlin.code.style=official
18 | # Dokka needs more memory than the default provides.
19 | org.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m
20 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | compileSdk = "android-29"
3 | targetSdk = "29"
4 | minimumSdk = "19"
5 | kotlin = "1.6.0"
6 | junit = "4.13.1"
7 | assertj = "2.6.0"
8 | mockito2 = "3.0.0"
9 | androidx_material = "1.0.0"
10 | androidx_annotation = "1.1.0"
11 | androidx_test_rules = "1.2.0"
12 | androidx_espresso = "3.2.0"
13 |
14 | [libraries]
15 | kotlin_stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
16 | junit4 = { module = "junit:junit", version.ref = "junit" }
17 | assertj_core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
18 | mockito_core = { module = "org.mockito:mockito-core", version.ref = "mockito2" }
19 | androidx_test_rules = { module = "androidx.test:rules", version.ref = "androidx_test_rules" }
20 | androidx_material = { module = "com.google.android.material:material", version.ref = "androidx_material" }
21 | androidx_annotation = { module = "androidx.annotation:annotation", version.ref = "androidx_annotation" }
22 | androidx_espresso_core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx_espresso" }
23 | androidx_espresso_idling_resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "androidx_espresso" }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/americanexpress/busybee/28925c25afc90fb7e185879d4f9e621350efedb8/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2020 American Express Travel Related Services Company, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | # in compliance with the License. You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software distributed under the License
10 | # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | # or implied. See the License for the specific language governing permissions and limitations under
12 | # the License.
13 | #
14 | distributionBase=GRADLE_USER_HOME
15 | distributionPath=wrapper/dists
16 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
17 | zipStoreBase=GRADLE_USER_HOME
18 | zipStorePath=wrapper/dists
19 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin, switch paths to Windows format before running java
129 | if $cygwin ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=$((i+1))
158 | done
159 | case $i in
160 | (0) set -- ;;
161 | (1) set -- "$args0" ;;
162 | (2) set -- "$args0" "$args1" ;;
163 | (3) set -- "$args0" "$args1" "$args2" ;;
164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=$(save "$@")
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
185 | cd "$(dirname "$0")"
186 | fi
187 |
188 | exec "$JAVACMD" "$@"
189 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
34 |
35 | @rem Find java.exe
36 | if defined JAVA_HOME goto findJavaFromJavaHome
37 |
38 | set JAVA_EXE=java.exe
39 | %JAVA_EXE% -version >NUL 2>&1
40 | if "%ERRORLEVEL%" == "0" goto init
41 |
42 | echo.
43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
44 | echo.
45 | echo Please set the JAVA_HOME variable in your environment to match the
46 | echo location of your Java installation.
47 |
48 | goto fail
49 |
50 | :findJavaFromJavaHome
51 | set JAVA_HOME=%JAVA_HOME:"=%
52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
53 |
54 | if exist "%JAVA_EXE%" goto init
55 |
56 | echo.
57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
58 | echo.
59 | echo Please set the JAVA_HOME variable in your environment to match the
60 | echo location of your Java installation.
61 |
62 | goto fail
63 |
64 | :init
65 | @rem Get command-line arguments, handling Windows variants
66 |
67 | if not "%OS%" == "Windows_NT" goto win9xME_args
68 |
69 | :win9xME_args
70 | @rem Slurp the command line arguments.
71 | set CMD_LINE_ARGS=
72 | set _SKIP=2
73 |
74 | :win9xME_args_slurp
75 | if "x%~1" == "x" goto execute
76 |
77 | set CMD_LINE_ARGS=%*
78 |
79 | :execute
80 | @rem Setup the command line
81 |
82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
83 |
84 | @rem Execute Gradle
85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
86 |
87 | :end
88 | @rem End local scope for the variables with windows NT shell
89 | if "%ERRORLEVEL%"=="0" goto mainEnd
90 |
91 | :fail
92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
93 | rem the _cmd.exe /c_ return code!
94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
95 | exit /b 1
96 |
97 | :mainEnd
98 | if "%OS%"=="Windows_NT" endlocal
99 |
100 | :omega
101 |
--------------------------------------------------------------------------------
/images/busybee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/americanexpress/busybee/28925c25afc90fb7e185879d4f9e621350efedb8/images/busybee.png
--------------------------------------------------------------------------------
/sample-app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/sample-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 |
15 | plugins {
16 | id("com.android.application")
17 | id("org.jetbrains.kotlin.android")
18 | }
19 |
20 | group = "io.americanexpress.busybee.app"
21 |
22 | android {
23 | compileSdkVersion = libs.versions.compileSdk.get()
24 | defaultConfig {
25 | minSdk = libs.versions.minimumSdk.get().toInt()
26 | targetSdk = libs.versions.targetSdk.get().toInt()
27 | version = 1
28 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
29 | }
30 |
31 | lint {
32 | isWarningsAsErrors = true
33 | disable("GoogleAppIndexingWarning", // not needed for a sample app
34 | "MissingApplicationIcon") // not needed for a sample app
35 | }
36 |
37 | buildTypes {
38 | getByName("release") {
39 | signingConfig = signingConfigs.getByName("debug")
40 | isMinifyEnabled = false
41 | }
42 | }
43 | }
44 |
45 | dependencies {
46 | implementation(project(":busybee-android"))
47 | implementation(libs.androidx.material)
48 | implementation(libs.kotlin.stdlib)
49 |
50 | androidTestImplementation(libs.androidx.annotation)
51 | androidTestImplementation(libs.androidx.espresso.core)
52 | androidTestImplementation(libs.androidx.espresso.idling.resource)
53 | androidTestImplementation(libs.androidx.test.rules)
54 | androidTestImplementation(libs.assertj.core)
55 | }
56 |
--------------------------------------------------------------------------------
/sample-app/src/androidTest/java/io/americanexpress/busybee/BusyBeeActivityTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee
15 |
16 | import androidx.test.espresso.Espresso.onView
17 | import androidx.test.espresso.action.ViewActions.click
18 | import androidx.test.espresso.assertion.ViewAssertions.matches
19 | import androidx.test.espresso.matcher.ViewMatchers.withId
20 | import androidx.test.espresso.matcher.ViewMatchers.withText
21 | import androidx.test.rule.ActivityTestRule
22 | import io.americanexpress.busybee.sample.BusyBeeActivity
23 | import org.junit.Before
24 | import org.junit.Test
25 |
26 | class BusyBeeActivityTest {
27 | private val activityRule = ActivityTestRule(BusyBeeActivity::class.java)
28 |
29 | @Before
30 | fun setUp() {
31 | activityRule.launchActivity(null)
32 | }
33 |
34 | @Test
35 | fun whenClickButton_thenEspressoWaitsForResultToBeDisplayed() {
36 | onView(withText(R.string.press_me)).perform(click())
37 | onView(withId(R.id.confirmation)).check(matches(withText("Button Pressed")))
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/sample-app/src/androidTest/java/io/americanexpress/busybee/BusyBeeCategoryTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee
15 |
16 | import android.os.Handler
17 | import android.os.Looper
18 | import android.view.View
19 | import android.widget.TextView
20 | import androidx.test.espresso.Espresso.onView
21 | import androidx.test.espresso.PerformException
22 | import androidx.test.espresso.action.ViewActions.click
23 | import androidx.test.espresso.assertion.ViewAssertions.matches
24 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
25 | import androidx.test.espresso.matcher.ViewMatchers.withText
26 | import androidx.test.rule.ActivityTestRule
27 | import io.americanexpress.busybee.BusyBee.Category.NETWORK
28 | import io.americanexpress.busybee.sample.BusyBeeActivity
29 | import org.junit.Assert.fail
30 | import org.junit.Before
31 | import org.junit.Test
32 |
33 | class BusyBeeCategoryTest {
34 |
35 | private val activityRule = ActivityTestRule(BusyBeeActivity::class.java)
36 | private val busyBee = BusyBee.singleton()
37 |
38 | companion object {
39 | const val idleTimeoutInMillis = 4000L
40 | const val SOME_BACKGROUND_OPERATION = "Some background Operation"
41 | const val NETWORK_BACKGROUND_OPERATION = "Network background Operation"
42 |
43 | private val MAIN_THREAD = Handler(Looper.getMainLooper())
44 | }
45 |
46 | @Before
47 | fun setUp() {
48 | val activity = activityRule.launchActivity(null)
49 |
50 | activity.onButtonClick(View.OnClickListener { view: View ->
51 | when (view) {
52 | is TextView -> {
53 | busyBee.busyWith(SOME_BACKGROUND_OPERATION)
54 | busyBee.busyWith(NETWORK_BACKGROUND_OPERATION, NETWORK)
55 | Thread(Runnable {
56 | try {
57 | Thread.sleep(idleTimeoutInMillis / 2)
58 | MAIN_THREAD.post { view.text = "All done." }
59 | } catch (e: InterruptedException) {
60 | throw RuntimeException(e)
61 | } finally {
62 | busyBee.completed(SOME_BACKGROUND_OPERATION)
63 | // Purposefully, don't complete the Network operation
64 | // busyBee.completed(NETWORK_BACKGROUND_OPERATION)
65 | }
66 | }).start()
67 | }
68 | else -> throw IllegalArgumentException()
69 | }
70 | })
71 |
72 | }
73 |
74 | @Test
75 | fun whenPayAttentionToNetwork_thenExceptionWhenClick() {
76 | busyBee.payAttentionToCategory(NETWORK)
77 | try {
78 | onView(withText(R.string.press_me)).perform(click())
79 | fail("should throw a perform exception because the network operation never completes")
80 | } catch (e: PerformException) {
81 | // expected
82 | }
83 | }
84 |
85 | @Test
86 | fun whenIgnoreNetwork_thenCanStillClickButton() {
87 | busyBee.ignoreCategory(NETWORK)
88 |
89 | onView(withText(R.string.press_me)).perform(click())
90 | onView(withText("All done.")).check(matches(isDisplayed()))
91 | }
92 | }
--------------------------------------------------------------------------------
/sample-app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
17 |
18 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/sample-app/src/main/java/io/americanexpress/busybee/sample/BusyBeeActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 American Express Travel Related Services Company, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package io.americanexpress.busybee.sample
15 |
16 | import android.os.Bundle
17 | import android.view.View
18 | import android.widget.Button
19 | import android.widget.TextView
20 | import androidx.appcompat.app.AppCompatActivity
21 | import io.americanexpress.busybee.BusyBee
22 | import io.americanexpress.busybee.R
23 | import java.util.concurrent.Executors
24 |
25 | class BusyBeeActivity : AppCompatActivity() {
26 | private val busyBee = BusyBee.singleton()
27 |
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 | setContentView(R.layout.activity_busy_bee)
31 | val confirmation = findViewById(R.id.confirmation)
32 | onButtonClick(View.OnClickListener {
33 | val foo = "FOO"
34 | busyBee.busyWith(foo)
35 | Executors.newSingleThreadExecutor().execute {
36 | Thread.sleep(1000)
37 | confirmation.post {
38 | confirmation.text = getString(R.string.button_pressed)
39 | busyBee.completed(foo)
40 | }
41 | }
42 | })
43 | }
44 |
45 | fun onButtonClick(clickListener: View.OnClickListener) {
46 | val button = findViewById