Browse Source

RD-213 implementation http proxy

master
bean 2 years ago
parent
commit
c243c36e4f
  1. 15
      .editorconfig
  2. 69
      README.md
  3. 10
      build.gradle.kts
  4. 17
      gradle/libs.versions.toml
  5. BIN
      img.png
  6. BIN
      img_1.png
  7. 21
      src/main/kotlin/tv/anypoint/ApplicationProperties.kt
  8. 6
      src/main/kotlin/tv/anypoint/domain/adb/ChangeCommand.kt
  9. 2
      src/main/kotlin/tv/anypoint/domain/adb/ExtraKey.kt
  10. 3
      src/main/kotlin/tv/anypoint/domain/adb/IntentAction.kt
  11. 20
      src/main/kotlin/tv/anypoint/domain/agent/AuthRequest.kt
  12. 112
      src/main/kotlin/tv/anypoint/domain/agent/AuthResponse.kt
  13. 34
      src/main/kotlin/tv/anypoint/domain/agent/Cue.kt
  14. 14
      src/main/kotlin/tv/anypoint/domain/agent/CueOwner.kt
  15. 7
      src/main/kotlin/tv/anypoint/domain/agent/PlayType.kt
  16. 6
      src/main/kotlin/tv/anypoint/domain/agent/PlayerStateCheckParam.kt
  17. 38
      src/main/kotlin/tv/anypoint/domain/agent/ProgramProviderChannel.kt
  18. 22
      src/main/kotlin/tv/anypoint/domain/agent/StateChangeLog.kt
  19. 28
      src/main/kotlin/tv/anypoint/domain/agent/ad/Ad.kt
  20. 31
      src/main/kotlin/tv/anypoint/domain/agent/ad/AdRequest.kt
  21. 14
      src/main/kotlin/tv/anypoint/domain/agent/ad/AdsResponse.kt
  22. 9
      src/main/kotlin/tv/anypoint/domain/agent/ad/AdsSyncRequest.kt
  23. 14
      src/main/kotlin/tv/anypoint/domain/agent/ad/Asset.kt
  24. 3
      src/main/kotlin/tv/anypoint/domain/agent/ad/AssetConvertResponse.kt
  25. 8
      src/main/kotlin/tv/anypoint/domain/agent/ad/Click.kt
  26. 21
      src/main/kotlin/tv/anypoint/domain/agent/ad/ProgramPlacement.kt
  27. 27
      src/main/kotlin/tv/anypoint/domain/agent/ad/TargetAd.kt
  28. 3
      src/main/kotlin/tv/anypoint/domain/agent/ad/VastResponse.kt
  29. 11
      src/main/kotlin/tv/anypoint/dsl/Dsl.kt
  30. 10
      src/main/kotlin/tv/anypoint/dsl/exception/HttpValidationException.kt
  31. 2
      src/main/kotlin/tv/anypoint/dsl/handler/HttpHandler.kt
  32. 5
      src/main/kotlin/tv/anypoint/dsl/model/RecordingInfo.kt
  33. 10
      src/main/kotlin/tv/anypoint/dsl/model/Tc.kt
  34. 6
      src/main/kotlin/tv/anypoint/dsl/model/http/AdsResponse.kt
  35. 6
      src/main/kotlin/tv/anypoint/dsl/model/http/AssetConvertResponse.kt
  36. 6
      src/main/kotlin/tv/anypoint/dsl/model/http/AuthResponse.kt
  37. 6
      src/main/kotlin/tv/anypoint/dsl/model/http/VastResponse.kt
  38. 19
      src/main/kotlin/tv/anypoint/dsl/serialization/JsonConfiguration.kt
  39. 24
      src/main/kotlin/tv/anypoint/dsl/serialization/LocalDateSerializer.kt
  40. 24
      src/main/kotlin/tv/anypoint/dsl/serialization/LocalDateTimeSerializer.kt
  41. 31
      src/main/kotlin/tv/anypoint/dsl/service/TestCase.kt
  42. 30
      src/main/kotlin/tv/anypoint/proxy/adapter/DeviceV3Adapter.kt
  43. 127
      src/main/kotlin/tv/anypoint/proxy/service/NetworkUtil.kt
  44. 62
      src/main/kotlin/tv/anypoint/proxy/web/DeviceController.kt
  45. 19
      src/main/kotlin/tv/anypoint/tc/Base1.kt
  46. 4
      src/main/kotlin/tv/anypoint/tc/Tc1.kt
  47. 23
      src/main/kotlin/tv/anypoint/tc/TestCaseStarter.kt
  48. 17
      src/main/resources/application.yaml
  49. 4
      src/test/kotlin/tv/anypoint/androidqa/AdsResponseTest.kt
  50. 13
      src/test/kotlin/tv/anypoint/androidqa/AndroidQaApplicationTests.kt
  51. 4
      src/test/kotlin/tv/anypoint/androidqa/AssetConvertResponseTest.kt
  52. 25
      src/test/kotlin/tv/anypoint/androidqa/AuthResponseTest.kt
  53. 4
      src/test/kotlin/tv/anypoint/androidqa/VastResponseTest.kt
  54. 98
      src/test/resources/ads-response.json
  55. 12
      src/test/resources/asset-convert.json
  56. 1580
      src/test/resources/auth-response.json
  57. 64
      src/test/resources/vast-response.xml

15
.editorconfig

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2

69
README.md

@ -0,0 +1,69 @@
## 프로젝트 구조
- domain: 순수한 도메인 관련 모델/기능
- dsl: TC DSL 관련 정의와 기능들
- proxy: 프록시 푸쉬 서버 관련 기능들
- STB 에 system command 와 cue 송신
- STB 의 auth, ads, vast, assetConvert 관련 HTTP 요청을 수신
- QA 들이 apache 를 사용하여 하던 행위를 대체
- TC 에서 HTTP 응답 변환이나 일시 정지, 딜레이가 필요할 경우 수행
- tc: TC 테스터와 TC 정의들
- 실제 TC 들을 정의
- 정의된 TC 들을 수행
## 실행 방법
1. STB 대여 (아래 사항들 확인 필요함)
- STB 가 접속되는 AP 이름과 비밀번호
- STB IP, adb 포트 번호
1. STB 설치
1. 컴퓨터의 USB 에 캡쳐보드를 꼽고 캡쳐보드와 STB output 과 연결
1. 컴퓨터에 obs-studio 설치 및 실행
2. 왼쪽 하단의 Sources 의 + 버튼(add sources) 클릭
![img.png](img.png)
3. video capture device(V4L2) (리눅스 기준) 클릭
4. 디바이스를 바꿔가면서 STB 를 찾아 선택 후 OK 클릭
![img_1.png](img_1.png)
1. application.yml 에서 anypoint.android-qa.stb 하위 정보들을 STB 의 정보로 수정
1. obs-studio 종료 후 android-qa 애플리케이션 실행 (obs-studio 종료해야 정상 작동됨)
## TC 수행 순서
1. adb 를 사용하여 STB 이 프록시 서버를 바라보도록 변경
```shell
adb shell am broadcast -a tv.anypoint.agent.app.CHANGE_TEST_PROPERTY --es change.command CHANGE_API_ENDPOINT --es api.endpoint http://<PROXY_SERVER_URL>:<PROXY_SERVER_PORT>
```
1.
## ffmpeg 리눅스에서 사용 방법
v4l2(video4linux2) 를 사용하여 캡쳐보드의 devicePath 구하기
```shell
$ v4l2-ctl --list-devices
Integrated Camera: Integrated C (usb-0000:00:14.0-8):
/dev/video0
/dev/video1
/dev/video2
/dev/video3
/dev/media0
/dev/media1
USB Video: USB Video (usb-0000:09:00.0-2.4):
/dev/video5
/dev/video6
/dev/media3
```
- 비디오: -f video4linux2 -i `<VIDEO_PATH>` 의 형식으로 입력.
- 오디오: -f alsa -ac 2 -i default 만 넣어도 되는데 캡쳐보드 전체가 동일한 지는 확인 안해 봄
아래 예제 참고
```shell
$ ffmpeg -f video4linux2 -i /dev/video5 -f alsa -ac 2 -i default -b:v 10M -vcodec h264_nvenc -pixel_format yuv420p -rtbufsize 1000M output_new_captureboard_30fps.mp4
```

10
build.gradle.kts

@ -21,10 +21,20 @@ repositories {
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test")
implementation(libs.bundles.deps)
testImplementation(libs.bundles.test.deps)
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.1")
}
}
tasks.withType<KotlinCompile> {

17
gradle/libs.versions.toml

@ -5,18 +5,27 @@ kotlin = "1.9.23"
jadb = { module = "com.github.vidstige:jadb", version = "v1.2.1" } # adb
netty = { module = "io.netty:netty-all", version = "4.1.96.Final" }
kotlinx-serializable = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.0" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.5.0" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.8.0" }
kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version = "2.0.11" }
thymeleaf = { module = "org.thymeleaf:thymeleaf", version = "3.1.2.RELEASE" }
thymeleaf-spring5 = { module = "org.thymeleaf:thymeleaf-spring5", version = "3.1.2.RELEASE" }
kotest-runner = { module = "io.kotest:kotest-runner-junit5-jvm", version = "5.8.1" }
kotest-property = { module = "io.kotest:kotest-property", version = "5.8.1" }
kotest-extensions-string = { module = "io.kotest.extensions:kotest-extensions-spring", version = "1.1.3" }
[bundles]
deps = [
"jadb",
"netty",
"kotlinx-serializable",
"kotlinx-datetime",
"kotlin-logging",
"thymeleaf",
"thymeleaf-spring5"
]
"thymeleaf-spring5",
"kotlinx-coroutines"
]
test-deps = [
"kotest-runner",
"kotest-property",
"kotest-extensions-string"
]

BIN
img.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
img_1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

21
src/main/kotlin/tv/anypoint/ApplicationProperties.kt

@ -6,5 +6,24 @@ import org.springframework.stereotype.Component
@Component
@ConfigurationProperties(prefix = "anypoint.android-qa")
class ApplicationProperties {
final lateinit var filePath: String
val stb: Stb = Stb()
/**
* STB logcat 덤프할 위치, 녹화된 영상을 저장할 위치
*/
final lateinit var fileRoot: String
final var pushServerPort: Int = 31102
val endpoints: Endpoints = Endpoints()
class Stb {
lateinit var ip: String
var port: Int = 5555
}
class Endpoints {
lateinit var auth: String
lateinit var assign: String
}
}

6
src/main/kotlin/tv/anypoint/domain/adb/ChangeCommand.kt

@ -0,0 +1,6 @@
package tv.anypoint.domain.adb
enum class ChangeCommand {
CHANGE_API_ENDPOINT,
CHANGE_TEST_FLAG
}

2
src/main/kotlin/tv/anypoint/dsl/model/adb/ExtraKey.kt → src/main/kotlin/tv/anypoint/domain/adb/ExtraKey.kt

@ -1,4 +1,4 @@
package tv.anypoint.dsl.model.adb
package tv.anypoint.domain.adb
enum class ExtraKey(val string: String) {
EXTRA_JSON("EXTRA_JSON"),

3
src/main/kotlin/tv/anypoint/dsl/model/adb/IntentAction.kt → src/main/kotlin/tv/anypoint/domain/adb/IntentAction.kt

@ -1,6 +1,5 @@
package tv.anypoint.dsl.model.adb
package tv.anypoint.domain.adb
enum class IntentAction(val string: String) {
CHANGE_TEST_PROPERTY("tv.anypoint.agent.app.CHANGE_TEST_PROPERTY")
;
}

20
src/main/kotlin/tv/anypoint/domain/agent/AuthRequest.kt

@ -0,0 +1,20 @@
package tv.anypoint.domain.agent
data class AuthRequest(
var fingerPrint: String = "",
val soId: Int = 0,
val uuid: String = "",
val usePersonalizedAd: Boolean = true,
val test: Boolean = false,
val zipCode: String = "",
val freeStorage: Long = 0,
val usedStorage: Long = 0,
val cachedStorage: Long = 0,
var modelName: String? = null,
val firmwareBuildDate: String? = null,
val firmwareVer: String? = null,
val fullFirmwareVer: String? = null,
val appVersion: String? = null,
val platformAdId: String? = null,
val sdkVersion: String? = null,
)

112
src/main/kotlin/tv/anypoint/domain/agent/AuthResponse.kt

@ -0,0 +1,112 @@
package tv.anypoint.domain.agent
import java.util.concurrent.TimeUnit
data class AuthResponse(
var deviceId: Long = 0,
var deviceTypeId: Int = 0,
var uuid: String = "",
var monitoringInterval: Long = 0,
val endpoints: Endpoints = Endpoints(),
var channels: List<ProgramProviderChannel> = emptyList(),
val adConfig: AdConfig = AdConfig(),
var chViewMinTime: Int = 0,
var kidWatermark: KidWatermark? = null,
val accessToken: String? = null,
val pushSecretKey: String = "Tx?c)T!3e.52Th5J",
val storeAppId: String? = null,
) {
companion object {
val DEFAULT_MONITOR_PERIOD = TimeUnit.MINUTES.toMillis(3)
private val MINIMUM_MONITOR_INTERVAL = TimeUnit.SECONDS.toMillis(30)
}
fun authorization() = "Anypoint ${accessToken ?: ""}"
fun monitoringInterval() = monitoringInterval.let {
if (it < MINIMUM_MONITOR_INTERVAL) DEFAULT_MONITOR_PERIOD else it
}
}
data class Endpoints(
var auth: String = "",
var requestAds: String = "",
var adSyncResult: String = "",
var appLog: String = "",
var event: String = "",
var pushServers: List<String> = emptyList(),
var stateLog: String = "",
var impressionLog: String = "",
var ntpServers: List<String> = emptyList(),
var assetRequest: String = "",
var proxyAdLog: String = "",
var externalDeviceMessageHost: String = "",
var externalDeviceMessagePort: Int = 0
)
enum class VideoPlayMode {
SERIALIZED, PARALLEL
}
enum class VideoMediaType {
CONCATENATING, LEGACY
}
// WARNING: local db(data class Device)의 Embedded 엔티티이므로 프로퍼티 변경 시, db 버전 올려야 함
data class AdConfig(
var maxDownloadBandwidth: Long = 150 * 1024,
var maxLazyDownloadBandwidth: Long = 1024 * 1024,
var appPath: String = "",
var maxUsableStorage: Long = 0,
var minFreeStorage: Long = 100 * 1024 * 1024,
var trackingRetryInterval: Int = 0,
var trackingRetryCount: Int = 0,
/**
* 남은 광고 시간 허용 기준 (해당 이하인 경우는 마지막 프레임을 유지하는 형태) 단위:
*/
var remnantTimeThreshold: Int = 2_000,
var maxEndAdPlaytime: Int = 15_000,
var transitionDelay: Int = 0,
var videoPlayMode: VideoPlayMode = VideoPlayMode.PARALLEL,
var videoMediaType: VideoMediaType = VideoMediaType.CONCATENATING,
var startDelay: Int = 0,
var stopDelay: Int = 0,
var startRenderDelay: Int = 0,
var stopRenderDelay: Int = 0,
var overPlayTimeThreshold: Int = 0,
// (플레이 리스트의 1번째 소재가 플레이 완료되기 전 x초)
// 해당 시간 전까지 준비 완료가 안되면 대체 소재로 플레이
// sdk 사용시 player의 버퍼 시간보다 1초 이상 커야 한다
// (sdk 내장 플레이어는 현재 3초)
var continuousCueMarginTime: Int = 34, // 연속 큐로 간주하기 위한 최대 cue gap
var minAdPlayAvailableMemMB: Int = 0,
var useSdkDigitalCue: Boolean = true,
var minCueGap: Int = TimeUnit.SECONDS.toMillis(5).toInt(),
var maxCueOutDelay: Int = TimeUnit.SECONDS.toMillis(15).toInt(),
var maxCueDuration: Int = TimeUnit.MINUTES.toMillis(10).toInt(),
var playerStateCheckParam: PlayerStateCheckParam = PlayerStateCheckParam(),
var useLastPositionGoogleAd: Boolean = false,
var retainedChannelStream: Boolean = true,
val maxAssignLoop: Int = 20,
val onCueCachingTimeWeight: Float = 0.5f,
var allowedUnfilledPlaylist: Boolean = false,
var allowedFirstUnreadyAd: Boolean = false,
var onceAdReadyResponse: Boolean = true,
var onceAdReadyRequiredTime: Int = 1000,
var minRequestAdDuration: Int = 5_000,
var sendBeaconFireResult: Boolean = false,
var onPlayDelay: Int = 0,
var skipOffset: Int = 5_000,
val useMultipleAssetConvert: Boolean = false,
)
// WARNING: local db(data class Device)의 Embedded 엔티티이므로 프로퍼티 변경 시, db 버전 올려야 함
data class KidWatermark(
var imageUrl: String = "",
var crc: Long = 0,
var left: Int = 0,
var top: Int = 0,
var width: Int = 0,
var height: Int = 0
)

34
src/main/kotlin/tv/anypoint/domain/agent/Cue.kt

@ -0,0 +1,34 @@
package tv.anypoint.domain.agent
data class Cue(
var id: Long = 0,
var ppId: Int = 0,
/** 큐 시작 시각 (ms) */
var time: Long = 0,
/** 큐 길이 (ms) */
var duration: Int = 0,
var type: CueType = CueType.NORMAL,
var placementId: Int = 0,
var startPts: Long = 0,
var multicastUri: String = "",
var isPushCue: Boolean = false,
var owner: CueOwner = CueOwner.OTT
) {
/** 큐 종료 시각 (ms) = [time] + [duration] */
val endTime: Long
get() = time + duration
}
enum class CueType(val v: Byte) {
INVALID_CUE_TYPE(Byte.MIN_VALUE),
NORMAL(0),
CANCEL(1),
REPLACE(2)
;
companion object {
operator fun get(v: Byte): CueType {
return values().find { it.v == v } ?: INVALID_CUE_TYPE
}
}
}

14
src/main/kotlin/tv/anypoint/domain/agent/CueOwner.kt

@ -0,0 +1,14 @@
package tv.anypoint.domain.agent
enum class CueOwner {
PP,
SO,
OTT
;
companion object {
fun fromCode(code: Int): CueOwner {
return CueOwner.values().last { it.ordinal == code }
}
}
}

7
src/main/kotlin/tv/anypoint/domain/agent/PlayType.kt

@ -0,0 +1,7 @@
package tv.anypoint.domain.agent
enum class PlayType {
DNP,
LAZY_DNP,
STREAMING
}

6
src/main/kotlin/tv/anypoint/domain/agent/PlayerStateCheckParam.kt

@ -0,0 +1,6 @@
package tv.anypoint.domain.agent
data class PlayerStateCheckParam(
val playerStateCheckDuration: Int = 1500,
val maxPlayerInvalidateInterval: Int = 300
)

38
src/main/kotlin/tv/anypoint/domain/agent/ProgramProviderChannel.kt

@ -0,0 +1,38 @@
package tv.anypoint.domain.agent
data class ProgramProviderChannel(
val id: Int,
val delay: Int = 0,
val scte35Delay: Int = 0,
val serviceId: String,
val kid: Boolean = false,
/** 없을 경우 service 채널 아님 */
val placementIds: IntArray? = null,
val testPlacementIds: IntArray? = null,
val taxonomies: List<String>? = null,
) {
var recordId: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ProgramProviderChannel
if (id != other.id) return false
if (delay != other.delay) return false
if (serviceId != other.serviceId) return false
if (kid != other.kid) return false
if (placementIds != null) {
if (other.placementIds == null) return false
if (!placementIds.contentEquals(other.placementIds)) return false
} else if (other.placementIds != null) return false
return true
}
override fun hashCode(): Int {
var result = id
result = 31 * result + delay
result = 31 * result + serviceId.hashCode()
result = 31 * result + kid.hashCode()
result = 31 * result + (placementIds?.contentHashCode() ?: 0)
return result
}
}

22
src/main/kotlin/tv/anypoint/domain/agent/StateChangeLog.kt

@ -0,0 +1,22 @@
package tv.anypoint.domain.agent
data class StateChangeLog(
val soId: Int = 0,
val deviceId: Long = 0,
val previousState: State? = null,
var currentState: State? = null
)
data class State(
val inTime: Long,
val outTime: Long? = null,
var state: TvState = TvState.ETC,
var appId: String? = null,
val channelServiceId: String? = null,
val vodTitle: String? = null,
var isCompleted: Boolean = false
)
enum class TvState {
HOME, LINEAR_TV, VOD, APP, SLEEP, ETC
}

28
src/main/kotlin/tv/anypoint/domain/agent/ad/Ad.kt

@ -0,0 +1,28 @@
package tv.anypoint.domain.agent.ad
data class Ad(
val id: Long,
val adRequest: AdRequest? = null,
var asset: Asset? = null,
val campaignId: Long = 0,
val campaignType: CampaignType = CampaignType.CHARGE,
val viewSequence: Int = 0,
var paused: Boolean = false,
var price: Float? = null,
var priceModel: String? = null,
var priceCurrency: String? = null,
val startAt: Long = 0,
val endAt: Long = 0,
val extraPlay: Int = 0,
val extraPlayContinuous: Boolean = false,
val click: Click? = null,
val exclusiveAdIds: Set<Long>? = null
)
enum class CampaignType {
CHARGE,
FREE,
HOUSE,
BUFFER,
ENDING
}

31
src/main/kotlin/tv/anypoint/domain/agent/ad/AdRequest.kt

@ -0,0 +1,31 @@
package tv.anypoint.domain.agent.ad
import tv.anypoint.domain.agent.PlayType
enum class EncodingType {
NONE,
MANUAL,
AUTO
}
enum class ExtAdProtocol {
VAST,
GOOGLE_AD
}
data class AdRequest(
var url: String,
val updateInterval: Int = 0,
val timeout: Int = 1000,
val encodingType: EncodingType = EncodingType.AUTO,
val extPlatformMappingId: Long = 0,
val onCue: Boolean = false,
var protocol: ExtAdProtocol = ExtAdProtocol.VAST,
val assetPlayType: PlayType = PlayType.DNP, // android room, field name duplication 방지
var maxDuration: Int = 15_000,
var minDuration: Int = 3_000,
val headers: Map<String, String>? = null,
var fallbackUrl: String? = null,
val fallbackProtocol: ExtAdProtocol = ExtAdProtocol.VAST,
)

14
src/main/kotlin/tv/anypoint/domain/agent/ad/AdsResponse.kt

@ -0,0 +1,14 @@
package tv.anypoint.domain.agent.ad
data class AdsResponse(
val status: AdListResponseStatus = AdListResponseStatus.OK,
val ads: List<Ad> = listOf(),
val targetAds: List<TargetAd> = listOf(),
val deleteCache: Boolean = false,
)
enum class AdListResponseStatus {
OK,
ERROR,
IGNORE_ERROR
}

9
src/main/kotlin/tv/anypoint/domain/agent/ad/AdsSyncRequest.kt

@ -0,0 +1,9 @@
package tv.anypoint.domain.agent.ad
data class AdsSyncRequest(
val deviceId: Long,
val freeStorage: Long,
val usedStorage: Long,
val cachedStorage: Long,
val adIds: List<Long>
)

14
src/main/kotlin/tv/anypoint/domain/agent/ad/Asset.kt

@ -0,0 +1,14 @@
package tv.anypoint.domain.agent.ad
import tv.anypoint.domain.agent.PlayType
data class Asset(
var assetId: Long = 0L,
var crc: String = "0",
var mediaUrl: String = "",
/** In milliseconds */
var duration: Int = 0,
var bytes: Long = 0,
var playType: PlayType = PlayType.DNP,
var extendedQueryString: String = ""
)

3
src/main/kotlin/tv/anypoint/domain/agent/ad/AssetConvertResponse.kt

@ -0,0 +1,3 @@
package tv.anypoint.domain.agent.ad
data class AssetConvertResponse(val id: Long)

8
src/main/kotlin/tv/anypoint/domain/agent/ad/Click.kt

@ -0,0 +1,8 @@
package tv.anypoint.domain.agent.ad
data class Click(
val targetType: String,
val targetValue: String,
val startDelay: Int = 0,
val endDelay: Int = 0
)

21
src/main/kotlin/tv/anypoint/domain/agent/ad/ProgramPlacement.kt

@ -0,0 +1,21 @@
package tv.anypoint.domain.agent.ad
data class ProgramPlacement(
val id: Int,
val placementUniqueKey: PlacementUniqueKey,
val durationInMs: Long,
val mediaType: MediaType,
val test: Boolean
)
data class PlacementUniqueKey(
val mediaId: Int,
val contentId: Int,
val programId: Int,
val platformId: Int,
val contentVendorId: String? = null
)
enum class MediaType {
SO, PP
}

27
src/main/kotlin/tv/anypoint/domain/agent/ad/TargetAd.kt

@ -0,0 +1,27 @@
package tv.anypoint.domain.agent.ad
data class TargetAd(
val hours: String? = null,
val days: String? = null,
val adIds: String,
var placements: String? = null
) {
companion object {
fun from(
hours: Set<Int> = (0..23).toSet(),
days: Set<Int> = (1..7).toSet(),
placements: Set<ProgramPlacement> = emptySet(),
adIds: Set<Int> = emptySet()
) = TargetAd(
hours = hours.joinWithVerticalBar(),
days = days.joinWithVerticalBar(),
placements = placements
.map { "${it.placementUniqueKey.contentId}-${it.placementUniqueKey.programId}" }
.joinWithVerticalBar(),
adIds = adIds.joinWithVerticalBar()
)
}
}
private fun <T> Iterable<T>.joinWithVerticalBar() = this.joinToString(prefix = "|", postfix = "|", separator = "|")

3
src/main/kotlin/tv/anypoint/domain/agent/ad/VastResponse.kt

@ -0,0 +1,3 @@
package tv.anypoint.domain.agent.ad
data class VastResponse(val id: Long)

11
src/main/kotlin/tv/anypoint/dsl/Dsl.kt

@ -1,22 +1,19 @@
package tv.anypoint.dsl
import tv.anypoint.domain.adb.ExtraKey
import tv.anypoint.domain.adb.IntentAction
import tv.anypoint.dsl.handler.HttpHandler
import tv.anypoint.dsl.model.adb.ExtraKey
import tv.anypoint.dsl.model.adb.IntentAction
import tv.anypoint.dsl.model.Tc
import tv.anypoint.dsl.service.TestCase
import java.time.Duration
import java.time.LocalDateTime
import java.time.Period
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
inline fun tc(
block: Tc.() -> Unit
) = Tc().also { block(it) }
inline fun <reified T>http(
inline fun <reified T> http(
block: HttpHandler<T>.() -> Unit
) = HttpHandler<T>().also { block(it) }
@ -47,4 +44,4 @@ fun TestCase.expected(
}
// TODO: tc 로그에서 expectedLog 찾기 tc.logInfo.cursor ~ 마지막 라인까지의 로그 중에 찾으면 됨
}
}
}

10
src/main/kotlin/tv/anypoint/dsl/exception/HttpValidationException.kt

@ -3,10 +3,10 @@ package tv.anypoint.dsl.exception
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.encodeToJsonElement
import tv.anypoint.dsl.model.http.AdsResponse
import tv.anypoint.dsl.model.http.AssetConvertResponse
import tv.anypoint.dsl.model.http.AuthResponse
import tv.anypoint.dsl.model.http.VastResponse
import tv.anypoint.domain.agent.AuthResponse
import tv.anypoint.domain.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AssetConvertResponse
import tv.anypoint.domain.agent.ad.VastResponse
class HttpValidationException(
type: HttpValidationExceptionType,
@ -26,7 +26,7 @@ enum class HttpValidationExceptionType {
inline fun <reified T> httpValidationError(
response: T
): HttpValidationException = HttpValidationException(
type = when(T::class) {
type = when (T::class) {
AuthResponse::class -> HttpValidationExceptionType.AUTH
AdsResponse::class -> HttpValidationExceptionType.ADS
VastResponse::class -> HttpValidationExceptionType.VAST

2
src/main/kotlin/tv/anypoint/dsl/handler/HttpHandler.kt

@ -1,6 +1,6 @@
package tv.anypoint.dsl.handler
class HttpHandler<R> (
class HttpHandler<R>(
var convert: (response: R) -> R = { it },
var validate: (response: R) -> Boolean = { true }
)

5
src/main/kotlin/tv/anypoint/dsl/model/RecordingInfo.kt

@ -1,10 +1,14 @@
package tv.anypoint.dsl.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.time.LocalDateTime
@Serializable
data class RecordingInfo(
val absoluteFilePath: String
) {
@Contextual
private val createdAt: LocalDateTime = LocalDateTime.now()
/**
@ -12,6 +16,7 @@ data class RecordingInfo(
*/
private var fileSize: Long = 0
@Contextual
private var finishedAt: LocalDateTime? = null
fun finish(fileSize: Long) {

10
src/main/kotlin/tv/anypoint/dsl/model/Tc.kt

@ -1,15 +1,15 @@
package tv.anypoint.dsl.model
import tv.anypoint.domain.agent.AuthResponse
import tv.anypoint.domain.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AssetConvertResponse
import tv.anypoint.domain.agent.ad.VastResponse
import tv.anypoint.dsl.handler.HttpHandler
import tv.anypoint.dsl.http
import tv.anypoint.dsl.model.http.AdsResponse
import tv.anypoint.dsl.model.http.AssetConvertResponse
import tv.anypoint.dsl.model.http.AuthResponse
import tv.anypoint.dsl.model.http.VastResponse
import java.time.LocalDateTime
class Tc {
var number: Int? = null
var number: String? = null
var reboot: Boolean = false
var auth: HttpHandler<AuthResponse> = http {}

6
src/main/kotlin/tv/anypoint/dsl/model/http/AdsResponse.kt

@ -1,6 +0,0 @@
package tv.anypoint.dsl.model.http
import kotlinx.serialization.Serializable
@Serializable
data class AdsResponse(val id: Long)

6
src/main/kotlin/tv/anypoint/dsl/model/http/AssetConvertResponse.kt

@ -1,6 +0,0 @@
package tv.anypoint.dsl.model.http
import kotlinx.serialization.Serializable
@Serializable
data class AssetConvertResponse(val id: Long)

6
src/main/kotlin/tv/anypoint/dsl/model/http/AuthResponse.kt

@ -1,6 +0,0 @@
package tv.anypoint.dsl.model.http
import kotlinx.serialization.Serializable
@Serializable
data class AuthResponse(val id: Long)

6
src/main/kotlin/tv/anypoint/dsl/model/http/VastResponse.kt

@ -1,6 +0,0 @@
package tv.anypoint.dsl.model.http
import kotlinx.serialization.Serializable
@Serializable
data class VastResponse(val id: Long)

19
src/main/kotlin/tv/anypoint/dsl/serialization/JsonConfiguration.kt

@ -0,0 +1,19 @@
package tv.anypoint.dsl.serialization
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.LocalDate
import java.time.LocalDateTime
@Configuration
class JsonConfiguration {
@Bean
fun json(): Json = Json {
serializersModule = SerializersModule {
contextual(LocalDate::class, LocalDateSerializer())
contextual(LocalDateTime::class, LocalDateTimeSerializer())
}
}
}

24
src/main/kotlin/tv/anypoint/dsl/serialization/LocalDateSerializer.kt

@ -0,0 +1,24 @@
package tv.anypoint.dsl.serialization
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.LocalDate
@OptIn(ExperimentalSerializationApi::class)
@Suppress("EXTERNAL_SERIALIZER_USELESS")
@Serializer(forClass = LocalDate::class)
class LocalDateSerializer : KSerializer<LocalDate> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDate) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): LocalDate = LocalDate.parse(decoder.decodeString())
}

24
src/main/kotlin/tv/anypoint/dsl/serialization/LocalDateTimeSerializer.kt

@ -0,0 +1,24 @@
package tv.anypoint.dsl.serialization
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.LocalDateTime
@OptIn(ExperimentalSerializationApi::class)
@Suppress("EXTERNAL_SERIALIZER_USELESS")
@Serializer(forClass = LocalDateTime::class)
class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): LocalDateTime = LocalDateTime.parse(decoder.decodeString())
}

31
src/main/kotlin/tv/anypoint/dsl/service/TestCase.kt

@ -4,13 +4,13 @@ import mu.KLogging
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import tv.anypoint.ApplicationProperties
import tv.anypoint.domain.agent.AuthResponse
import tv.anypoint.domain.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AssetConvertResponse
import tv.anypoint.domain.agent.ad.VastResponse
import tv.anypoint.dsl.exception.httpValidationError
import tv.anypoint.dsl.model.LogInfo
import tv.anypoint.dsl.model.RecordingInfo
import tv.anypoint.dsl.model.http.AdsResponse
import tv.anypoint.dsl.model.http.AssetConvertResponse
import tv.anypoint.dsl.model.http.AuthResponse
import tv.anypoint.dsl.model.http.VastResponse
import tv.anypoint.dsl.model.Tc
import java.time.LocalDateTime
@ -32,18 +32,19 @@ abstract class TestCase {
private lateinit var captureBoardRecorder: CaptureBoardRecorder
fun executeTest() {
startToDumpLog()
tc = init()
logger.info("[TC-${tc.number}] starting...")
logger.info("[${tc.number}] starting...")
try {
if (tc.reboot) {
logger.info("[TC-${tc.number}] reboot STB")
logger.info("[${tc.number}] reboot STB")
// TODO
} else {
logger.info("[TC-${tc.number}] get systemInfo STB")
logger.info("[${tc.number}] get systemInfo STB")
// TODO
}
startToDumpLog()
auth()
ads()
@ -55,23 +56,23 @@ abstract class TestCase {
stopToDumpLog()
} catch (e: Exception) {
logger.error("[TC-${tc.number}] FAILED!! ${e.message}", e)
logger.error("[${tc.number}] FAILED!! ${e.message}", e)
tc.result = false
return
} finally {
tc.finishedAt = LocalDateTime.now()
}
logger.info("[TC-${tc.number}] SUCCEED!!")
logger.info("[${tc.number}] SUCCEED!!")
tc.result = true
}
private fun startToDumpLog() {
tc.logInfo = LogInfo("${applicationProperties.filePath}/TC-${tc.number}.log")
// TODO 이 함수 호출된 후 main.log 에서 쌓이는 로그를 TC-${tc.number}.log 파일 새로 만들어서 적재 시작
tc.logInfo = LogInfo("${applicationProperties.fileRoot}/${tc.number}.log")
// TODO 이 함수 호출된 후 main.log 에서 쌓이는 로그를 ${tc.number}.log 파일 새로 만들어서 적재 시작
}
private fun stopToDumpLog() {
// TODO main.log >> TC-${tc.number}.log 적재 종료
// TODO main.log >> ${tc.number}.log 적재 종료
}
private fun auth() {
@ -86,7 +87,7 @@ abstract class TestCase {
private fun ads() {
// TODO: auth
val actualResponse = AdsResponse(0)
val actualResponse = AdsResponse()
val response = tc.ads.convert(actualResponse)
if (!tc.ads.validate(response)) {
throw httpValidationError(response)
@ -115,7 +116,7 @@ abstract class TestCase {
}
private fun startRecording(tc: Tc) {
tc.recordingInfo = RecordingInfo("${applicationProperties.filePath}/TC-${tc.number}.mp4")
tc.recordingInfo = RecordingInfo("${applicationProperties.fileRoot}/${tc.number}.mp4")
logger.debug("start to record. info: {}", tc.recordingInfo)
// TODO
}

30
src/main/kotlin/tv/anypoint/proxy/adapter/DeviceV3Adapter.kt

@ -0,0 +1,30 @@
package tv.anypoint.proxy.adapter
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import tv.anypoint.domain.agent.AuthRequest
import tv.anypoint.domain.agent.AuthResponse
import tv.anypoint.domain.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AdsSyncRequest
@FeignClient(
name = "DeviceV3Adapter",
path = "/v3/device"
)
interface DeviceV3Adapter {
@PostMapping("/auth")
fun auth(@RequestBody body: AuthRequest): AuthResponse
@PostMapping("/ads")
fun ads(
@RequestParam(required = true) deviceId: Long,
@RequestParam("freeStorage", required = true, defaultValue = "0") freeStorage: Long = 0,
@RequestParam("usedStorage", required = true, defaultValue = "0") usedStorage: Long = 0
): AdsResponse
@PostMapping("/sync/ads")
fun syncAds(@RequestBody request: AdsSyncRequest)
}

127
src/main/kotlin/tv/anypoint/proxy/service/NetworkUtil.kt

@ -0,0 +1,127 @@
package tv.anypoint.proxy.service
import jakarta.servlet.http.HttpServletRequest
import kotlinx.coroutines.*
import java.net.Inet4Address
import java.net.InetAddress
import java.net.NetworkInterface
import java.util.*
object NetworkUtil {
fun getPrivateIpV4(setTopBoxAddress: InetAddress): String = getPrivateIpV4List()
.chooseSimilarIp(setTopBoxAddress)?.hostAddress
?: throw RuntimeException()
/**
* Gives the private IP of this device
* @return The private IP
*/
fun getPrivateIpV4List(): List<InetAddress> {
val result = mutableListOf<InetAddress>()
NetworkInterface.getNetworkInterfaces().forEach { n ->
result += n.inetAddresses.filter { it is Inet4Address && it.isSiteLocalAddress }
}
return result
}
/**
* Gives the list of all open IPs of the private network
* in which this device is connected to.
*
* For the time saving, this method performs multithreaded
* ping test for 5 seconds in each thread, excluding
* 192.168.0.1 and this device's private IP.
*
* @return List of all open IPs as [InetAddress]
*/
fun findAllOpenPrivateIps(skipSelf: Boolean = false) = runBlocking {
val todos = mutableListOf<Deferred<Unit?>>()
val privateIpList = getPrivateIpV4List()
val available2dArray = Array(privateIpList.size) { arrayOfNulls<InetAddress>(256) }
privateIpList.forEachIndexed { i, inetAddress ->
todos += Inet4Iterator(inetAddress, skipSelf)
.map {
async(Dispatchers.IO) {
if (it.second.isReachable(1000)) available2dArray[i][it.first] = it.second else null
}
}
}
todos.awaitAll()
return@runBlocking available2dArray.flatten().filterNotNull()
}
/**
* The iterator that iterates all near IPs from the given [originalIp]
*
* If the given IP is a.b.c.d, then it will iterate from
* a.b.c.2 to a.b.c.255, when start=2 and [end]=255 (default)
*/
class Inet4Iterator(
private val originalIp: InetAddress,
private val skipOriginal: Boolean = true,
start: UInt = 2u,
private val end: UInt = 255u
) : Iterator<Pair<Int, InetAddress>>, Iterable<Pair<Int, InetAddress>> {
private val a: UByte
private val b: UByte
private val c: UByte
private val skip: UByte
private var d = start
init {
val addressBytes = originalIp.address
a = addressBytes[0].toUByte()
b = addressBytes[1].toUByte()
c = addressBytes[2].toUByte()
skip = addressBytes[3].toUByte()
}
override fun hasNext(): Boolean {
return if (skipOriginal && skip == 0xFFu.toUByte()) d < end else d < (end + 1u)
}
override fun next(): Pair<Int, InetAddress> {
val result = Pair(d.toInt(), Inet4Util.fromIpClasses(a, b, c, d.toUByte()))
d += if (d == skip - 1u) 2u else 1u
return result
}
override fun iterator() = this
}
object Inet4Util {
fun fromIpClasses(a: UByte, b: UByte, c: UByte, d: UByte): InetAddress {
return Inet4Address.getByName("$a.$b.$c.$d")
}
}
fun InetAddress.getIpClassSimilarity(other: InetAddress): Int {
if (this::class != other::class) return 0
val ipClasses = this.address
val otherIpClasses = other.address
for (i in 0 until ipClasses.size.coerceAtMost(otherIpClasses.size)) {
if (ipClasses[i] != otherIpClasses[i]) return i
}
return ipClasses.size
}
fun List<InetAddress>.chooseSimilarIp(other: InetAddress) = this.maxByOrNull { it.getIpClassSimilarity(other) }
fun <T> Enumeration<T>.forEach(block: (T) -> Unit) {
for (i in this) block(i)
}
fun <T> Enumeration<T>.filter(block: (T) -> Boolean): List<T> {
val result = mutableListOf<T>()
for (i in this) if (block(i)) result += i
return result
}
}
fun HttpServletRequest.getIpAddressString(): String = this.getHeader("X-FORWARDED-FOR") ?: this.remoteAddr
fun HttpServletRequest.getIpAddress(): InetAddress = InetAddress.getByName(this.getIpAddressString())

62
src/main/kotlin/tv/anypoint/proxy/web/DeviceController.kt

@ -0,0 +1,62 @@
package tv.anypoint.proxy.web
import jakarta.servlet.http.HttpServletRequest
import mu.KLogging
import org.springframework.web.bind.annotation.*
import tv.anypoint.domain.agent.AuthRequest
import tv.anypoint.domain.agent.AuthResponse
import tv.anypoint.domain.agent.StateChangeLog
import tv.anypoint.domain.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AdsSyncRequest
@RestController
@RequestMapping("/v3/device")
class DeviceController {
@PostMapping("/auth")
fun authorize(request: HttpServletRequest, @RequestBody body: AuthRequest): AuthResponse {
logger.info("POST /v3/device/auth -d $body")
// request.getIpAddress()
val authResponse: AuthResponse = TODO("authRequestHandlerService.handleRequest(HttpRequestUtil.getIpAddress(request), body)")
authResponse.adConfig.maxDownloadBandwidth = 1000 * 1024 * 1024
authResponse.adConfig.maxLazyDownloadBandwidth = 1000 * 1024 * 1024
return authResponse
}
@GetMapping("/ads")
fun advertisements(
@RequestParam("deviceId") deviceId: Long,
@RequestParam("freeStorage", required = false, defaultValue = "10000000000") freeStorage: Long?,
@RequestParam("usedStorage", required = false, defaultValue = "0") usedStorage: Long?
): AdsResponse {
logger.info("GET /v3/device/ads?deviceId=$deviceId&freeStorage=$freeStorage&usedStorage=$usedStorage")
// return adsRequestHandlerService.handleRequest(deviceId, freeStorage, usedStorage)
return AdsResponse()
}
@PostMapping("/sync/ads")
fun adSync(request: HttpServletRequest, @RequestBody body: AdsSyncRequest) {
logger.info("POST /v3/device/sync/ads -d $body")
// deviceAdSyncHandlerService.handleRequest(HttpRequestUtil.getIpAddress(request))
}
@PostMapping("/state-logs")
fun stateLogs(@RequestBody body: List<StateChangeLog>) {
logger.info("POST /v3/device/state-logs -d $body")
}
@PostMapping("/event")
@ResponseBody
fun deviceEvent(request: HttpServletRequest, @RequestBody body: String) {
logger.info("POST /v3/device/event -d $body")
}
@PostMapping("/impression-logs")
@ResponseBody
fun impressionLogs(request: HttpServletRequest, @RequestBody body: String) {
logger.info("POST /v3/device/ssion-logs -d $body")
}
companion object : KLogging()
}

19
src/main/kotlin/tv/anypoint/tc/Base1.kt

@ -2,21 +2,22 @@ package tv.anypoint.tc
import mu.KLogging
import org.springframework.stereotype.Component
import tv.anypoint.domain.adb.ExtraKey
import tv.anypoint.domain.adb.IntentAction
import tv.anypoint.dsl.adb
import tv.anypoint.dsl.expected
import tv.anypoint.dsl.http
import tv.anypoint.dsl.model.Tc
import tv.anypoint.dsl.service.TestCase
import tv.anypoint.dsl.tc
import tv.anypoint.dsl.model.Tc
import tv.anypoint.dsl.model.adb.ExtraKey
import tv.anypoint.dsl.model.adb.IntentAction
@Component
class Base1 : TestCase() {
override fun init(): Tc = tc {
number = 1
number = "BASE-1"
reboot = true
vast = http {
validate = { false }
validate = { it.id != null }
}
}
@ -25,8 +26,12 @@ class Base1 : TestCase() {
a = IntentAction.CHANGE_TEST_PROPERTY,
es = mapOf(ExtraKey.TEST_DEVICE to "true")
)
logger.debug("started to base1 test")
logger.debug("finished to base1 test")
expected("changed to test device")
// AD Agent, AD SDK 통신규격상으로는 a 필수, es 는 선택사항
// https://dev-docs.anypoint.tv/books/ff-settop-ad-agent/page/ad-agent-ad-sdk
// 예외 사항이 존재할 지?
adb(
a = IntentAction.CHANGE_TEST_PROPERTY,
es = mapOf(ExtraKey.TEST_DEVICE to "false")

4
src/main/kotlin/tv/anypoint/tc/Tc1.kt

@ -2,14 +2,14 @@ package tv.anypoint.tc
import mu.KLogging
import org.springframework.stereotype.Component
import tv.anypoint.dsl.model.Tc
import tv.anypoint.dsl.service.TestCase
import tv.anypoint.dsl.tc
import tv.anypoint.dsl.model.Tc
@Component
class Tc1 : TestCase() {
override fun init(): Tc = tc {
number = 1
number = "TC-1"
}
override fun test() {

23
src/main/kotlin/tv/anypoint/dsl/TestCaseStarter.kt → src/main/kotlin/tv/anypoint/tc/TestCaseStarter.kt

@ -1,19 +1,18 @@
package tv.anypoint.dsl
package tv.anypoint.tc
import mu.KLogging
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component
import org.springframework.stereotype.Service
import tv.anypoint.dsl.service.TestCase
@Component
@Service
class TestCaseStarter(
private val testCases: List<TestCase>
) {
@EventListener(ApplicationReadyEvent::class)
fun testAfterStartup() {
fun testAll() {
// TODO: main.log 덤프 시작
startDumpLog()
testCases.forEach {
it.executeTest()
}
@ -33,6 +32,16 @@ class TestCaseStarter(
)
)
}
stopDumpLog()
}
fun startDumpLog() {
// TODO:
}
fun stopDumpLog() {
// TODO
}
companion object : KLogging()

17
src/main/resources/application.yaml

@ -1,9 +1,24 @@
spring:
application:
name: android-qa
profiles:
active: skb
logging:
level:
root: INFO
tv.anypoint: DEBUG
anypoint.android-qa:
file-path: /home/bean/dev/qa
stb:
ip: 192.168.0.1
port: 5555
file-root: /home/bean/dev/qa
---
spring:
config.activate.on-profile: skb
anypoint.android-qa:
push-server-port: 31102
endpoints:
auth: https://skb-api.anypoint.tv
assign: http://skb-assign-app-prod.ap-northeast-1.elasticbeanstalk.com

4
src/test/kotlin/tv/anypoint/androidqa/AdsResponseTest.kt

@ -0,0 +1,4 @@
package tv.anypoint.androidqa
class AdsResponseTest {
}

13
src/test/kotlin/tv/anypoint/androidqa/AndroidQaApplicationTests.kt

@ -1,13 +0,0 @@
package tv.anypoint.androidqa
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class AndroidQaApplicationTests {
@Test
fun contextLoads() {
}
}

4
src/test/kotlin/tv/anypoint/androidqa/AssetConvertResponseTest.kt

@ -0,0 +1,4 @@
package tv.anypoint.androidqa
class AssetConvertResponseTest {
}

25
src/test/kotlin/tv/anypoint/androidqa/AuthResponseTest.kt

@ -0,0 +1,25 @@
package tv.anypoint.androidqa
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.springframework.core.io.ClassPathResource
import tv.anypoint.domain.agent.AuthResponse
@OptIn(ExperimentalSerializationApi::class)
class AuthResponseTest : BehaviorSpec() {
init {
Given("auth-response.json file") {
val file = ClassPathResource("auth-response.json").file
When("deserialize") {
val actual = Json.decodeFromStream<AuthResponse>(file.inputStream())
Then("read values") {
actual.deviceId shouldBe 36986237L
}
}
}
}
}

4
src/test/kotlin/tv/anypoint/androidqa/VastResponseTest.kt

@ -0,0 +1,4 @@
package tv.anypoint.androidqa
class VastResponseTest {
}

98
src/test/resources/ads-response.json

@ -0,0 +1,98 @@
{
"status": "OK",
"ads": [
{
"id": 66192,
"adRequest": null,
"asset": {
"assetId": 724560,
"crc": "1081046660",
"mediaUrl": "https://ecdnaddtv.hanafostv.com:9443/addrads/prod/72456/ENCODING/18/1672300537007.ts",
"duration": 14948,
"bytes": 7238188,
"playType": "DNP",
"extendedQueryString": "vid_bitrate=3817000&vid_color_bits=8&vid_codec=27&vid_profile=100&vid_level=40&vid_height=1080&vid_id=481&vid_scan=4&vid_width=1920&aud1_bitrate=3817000&aud1_ch=2&aud1_codec=15&aud1_profile=LC&aud1_id=482&aud1_sr=48000&aud2_bitrate=3817000&aud2_ch=2&aud2_codec=129&aud2_id=483&aud2_sr=48000&"
},
"campaignId": 26627,
"campaignType": "CHARGE",
"viewSequence": 5400,
"paused": false,
"price": 0,
"priceModel": "CPV",
"priceCurrency": "KRW",
"startAt": 1675177200000,
"endAt": 2145884399999,
"extraPlay": 0,
"extraPlayContinuous": false
},
{
"id": 48341,
"adRequest": null,
"asset": {
"assetId": 684610,
"crc": "1859527903",
"mediaUrl": "https://ecdnaddtv.hanafostv.com:9443/addrads/prod/68457/ENCODING/18/1668564528712.ts",
"duration": 2936,
"bytes": 498576,
"playType": "DNP",
"extendedQueryString": "vid_bitrate=1334000&vid_color_bits=8&vid_codec=27&vid_profile=100&vid_level=40&vid_height=1080&vid_id=481&vid_scan=4&vid_width=1920&aud1_bitrate=1334000&aud1_ch=2&aud1_codec=15&aud1_profile=LC&aud1_id=482&aud1_sr=48000&aud2_bitrate=1334000&aud2_ch=2&aud2_codec=129&aud2_id=483&aud2_sr=48000&"
},
"campaignId": 2645,
"campaignType": "ENDING",
"viewSequence": 0,
"paused": false,
"price": 0,
"priceModel": "CPV",
"priceCurrency": "KRW",
"startAt": 1606834800000,
"endAt": 2145884399999,
"extraPlay": 0,
"extraPlayContinuous": false
}
],
"targetAds": [
{
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|",
"days": "|1|2|3|4|5|6|7|",
"placements": "|100-1|109-1|112-2|115-1|118-1|121-1|124-1|133-1|136-1|142-1|154-1|157-1|160-2|163-1|169-1|172-1|178-1|181-1|184-1|187-1|190-1|193-1|196-1|199-1|205-1|208-1|211-1|223-1|232-1|235-1|238-1|241-1|244-1|247-1|250-1|253-1|307-1|316-2|325-2|328-1|337-1|340-2|343-1|346-1|349-1|352-1|355-1|361-1|364-1|367-1|37-1|40-1|424-1|427-1|43-1|430-1|433-1|436-1|46-1|469-1|49-1|499-2|511-1|512-1|514-1|515-1|524-1|541-1|728-1|773-1|774-1|784-1|79-1|809-1|82-1|85-1|88-1|883-1|885-1|887-200|888-200|889-1|91-1|94-1|97-1|",
"adIds": "|66437|"
},
{
"hours": "|0|1|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|",
"days": "|1|2|3|4|5|6|7|",
"placements": "|262-1|379-1|382-1|385-1|388-1|397-1|406-1|409-1|415-2|418-1|421-1|",
"adIds": "|66192|"
},
{
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|",
"days": "|1|2|3|4|5|6|7|",
"placements": "|322-1000|322-1100|322-1200|322-1300|322-2000|322-2100|322-2200|322-2300|322-3000|322-3100|322-3200|322-3300|",
"adIds": "|-54695|-54694|"
},
{
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|",
"days": "|1|2|3|4|5|6|7|",
"placements": "|13-100|13-110|13-120|13-130|",
"adIds": "|-54561|-54560|-54559|-54558|-54557|-54556|"
},
{
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|",
"days": "|1|2|3|4|5|6|7|",
"placements": "|190-2|",
"adIds": "|-53060|"
},
{
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|",
"days": "|1|2|3|4|5|6|7|",
"placements": "|100-1|109-1|109-2|112-2|112-5|115-1|118-1|121-1|121-2|124-1|133-1|136-1|139-1|142-1|154-1|157-1|160-2|163-1|169-1|172-1|175-1|178-1|181-1|184-1|187-1|190-1|190-2|193-1|196-1|199-1|205-1|208-1|211-1|211-2|214-1|217-1|217-2|220-1|223-1|226-1|229-1|232-1|235-1|235-2|238-1|241-1|241-2|244-1|244-2|247-1|250-1|250-2|253-1|253-2|259-1|262-1|274-1|277-1|289-1|298-1|307-1|310-1|316-2|316-5|322-1|322-1000|322-1100|322-1200|322-1300|322-2000|322-2100|322-2200|322-2300|322-3000|322-3100|322-3200|322-3300|325-2|325-5|328-1|331-1|337-1|340-2|340-5|343-1|343-2|346-1|349-1|349-2|352-1|355-1|358-1|361-1|364-1|367-1|37-1|379-1|379-2|382-1|385-1|388-1|397-1|40-1|406-1|409-1|415-2|415-5|418-1|421-1|424-1|427-1|43-1|430-1|433-1|436-1|46-1|469-1|469-5|487-1|49-1|499-2|511-1|512-1|514-1|514-2|515-1|524-1|524-2|541-1|545-2|687-2|691-1|691-1000|691-1100|691-1200|691-1300|691-2000|691-2100|691-2200|691-2300|691-3000|691-3100|691-3200|691-3300|723-2|728-1|728-2|734-2|735-2|736-2|773-1|773-2|774-1|784-1|79-1|79-2|796-2|797-2|809-1|809-2|814-2|82-1|85-1|88-1|883-1|883-2|885-1|885-1200|885-2200|885-3200|887-1000|887-1100|887-1200|887-1300|887-200|887-2000|887-2100|887-2200|887-2300|887-3000|887-3100|887-3200|887-3300|888-1100|888-1200|888-1300|888-2|888-200|889-1|889-1000|889-2000|889-3000|891-1200|891-2200|891-3200|91-1|94-1|97-1|",
"adIds": "|45174|45175|48490|52835|66003|-54714|"
},
{
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|",
"days": "|1|2|3|4|5|6|7|",
"placements": "|10-100|10-110|10-120|10-130|100-1|109-1|109-2|112-2|112-5|115-1|118-1|121-1|121-2|124-1|13-10|13-100|13-110|13-120|13-130|133-1|136-1|139-1|142-1|154-1|154-5|157-1|160-2|160-5|163-1|169-1|172-1|175-1|178-1|181-1|184-1|187-1|190-1|190-2|193-1|196-1|199-1|205-1|208-1|211-1|211-2|214-1|217-1|217-2|220-1|223-1|226-1|229-1|232-1|235-1|235-2|238-1|241-1|241-2|244-1|244-2|247-1|250-1|250-2|253-1|253-2|259-1|262-1|274-1|277-1|289-1|298-1|307-1|310-1|316-2|316-5|322-1|322-1000|322-1100|322-1200|322-1300|322-2000|322-2100|322-2200|322-2300|322-3000|322-3100|322-3200|322-3300|325-2|325-5|328-1|331-1|337-1|340-2|340-5|343-1|343-2|346-1|349-1|349-2|352-1|355-1|358-1|361-1|364-1|367-1|37-1|379-1|379-2|382-1|385-1|388-1|397-1|40-1|406-1|409-1|415-2|415-5|418-1|421-1|424-1|427-1|43-1|430-1|433-1|436-1|46-1|469-1|469-5|487-1|49-1|499-2|499-5|511-1|512-1|514-1|514-2|515-1|524-1|524-2|541-1|545-2|687-2|691-1|691-1000|691-1100|691-1200|691-1300|691-2000|691-2100|691-2200|691-2300|691-3000|691-3100|691-3200|691-3300|723-2|728-1|728-2|734-2|735-2|736-2|773-1|773-2|774-1|784-1|79-1|79-2|796-2|797-2|809-1|809-2|814-2|82-1|85-1|88-1|883-1|883-2|885-1|885-1200|885-2200|885-3200|887-1000|887-1100|887-1200|887-1300|887-200|887-2000|887-2100|887-2200|887-2300|887-3000|887-3100|887-3200|887-3300|888-1100|888-1200|888-1300|888-2|888-200|889-1|889-1000|889-2000|889-3000|891-1200|891-2200|891-3200|91-1|94-1|97-1|",
"adIds": "|48341|66003|67280|66192|66361|66133|66577|66260|-54714|67368|67367|-55857|67149|67147|67343|67370|"
}
],
"deleteCache": false
}

12
src/test/resources/asset-convert.json

@ -0,0 +1,12 @@
{
"created": false,
"asset": {
"assetId": 30116,
"crc": "1624645205",
"duration": 15000,
"mediaUrl": "http://123.140.50.10/prod/30116/ENCODING/18/1646959017272.mp4",
"playType": "DNP",
"bytes": 6741195,
"extendedQueryString": "audioPID=1002&audioType=AAC&videoPID=1001&videoType=H.264"
}
}

1580
src/test/resources/auth-response.json

File diff suppressed because it is too large Load Diff

64
src/test/resources/vast-response.xml

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<VAST version="3.0" xmlns="http://www.iab.net/videosuite/vast">
<Ad id="labZCxzVezmmzCFJ..leyIgUkNyTxpxAr2">
<InLine>
<AdSystem version="2.0"></AdSystem>
<AdTitle>
<![CDATA[labZCxzVezmmzCFJ..leyIgUkNyTxpxAr2]]>
</AdTitle>
<Description>
<![CDATA[]]>
</Description>
<Pricing model="CPM" currency="USD">5.101</Pricing>
<Survey>
<![CDATA[]]>
</Survey>
<Impression>
<![CDATA[http://192.168.10.13/grafter/imp?exchange=FLOWER&info=ChD_DOlhfBdKt6LFKb5wetkzEKG2ypoGGigICiIkMTJhZTQwNzUtZTA4MC01NjI1LWE4OTItZGFiZWFlZmUyMTNlIAIqADIA&campaign_name=rK5xhArs7Pvy2baC&imp_id=fg.req.id-34c0f17f-2440-42c0-bee0-3d2149aefc9b&price=5.101]]>
</Impression>
<Creatives>
<Creative>
<Linear>
<Duration>00:00:15</Duration>
<TrackingEvents>
<Tracking event="firstQuartile">
<![CDATA[http://192.168.10.13/grafter/imp_extra?exchange=FLOWER&info=ChD_DOlhfBdKt6LFKb5wetkzEKG2ypoGGigICiIkMTJhZTQwNzUtZTA4MC01NjI1LWE4OTItZGFiZWFlZmUyMTNlIAIqADIA&event_type=play_1q]]>
</Tracking>
<Tracking event="midpoint">
<![CDATA[http://192.168.10.13/grafter/imp_extra?exchange=FLOWER&info=ChD_DOlhfBdKt6LFKb5wetkzEKG2ypoGGigICiIkMTJhZTQwNzUtZTA4MC01NjI1LWE4OTItZGFiZWFlZmUyMTNlIAIqADIA&event_type=play_2q]]>
</Tracking>
<Tracking event="thirdQuartile">
<![CDATA[http://192.168.10.13/grafter/imp_extra?exchange=FLOWER&info=ChD_DOlhfBdKt6LFKb5wetkzEKG2ypoGGigICiIkMTJhZTQwNzUtZTA4MC01NjI1LWE4OTItZGFiZWFlZmUyMTNlIAIqADIA&event_type=play_3q]]>
</Tracking>
<Tracking event="complete">
<![CDATA[http://192.168.10.13/grafter/imp_extra?exchange=FLOWER&info=ChD_DOlhfBdKt6LFKb5wetkzEKG2ypoGGigICiIkMTJhZTQwNzUtZTA4MC01NjI1LWE4OTItZGFiZWFlZmUyMTNlIAIqADIA&event_type=play_4q]]>
</Tracking>
<Tracking event="start">
<![CDATA[http://192.168.10.13/grafter/start?id=fg.req.id-34c0f17f-2440-42c0-bee0-3d2149aefc9b&p=4&au=31&dsp=5&c=labZCxzVezmmzCFJ..leyIgUkNyTxpxAr2]]>
</Tracking>
</TrackingEvents>
<MediaFiles>
<MediaFile id="labZCxzVezmmzCFJ..leyIgUkNyTxpxAr2" delivery="progressive" type="video/mp4" width="1920" height="1080">
<![CDATA[http://123.140.50.10/prod/37084/ENCODING/18/138952160385610248.mp4]]>
</MediaFile>
</MediaFiles>
</Linear>
</Creative>
<Creative>
<Linear>
<TrackingEvents/>
<VideoClicks/>
<MediaFiles/>
</Linear>
</Creative>
</Creatives>
<Extensions>
<Extension type="FlowerAsset">
<Asset xmlns="">
{"assetId":-50430,"crc":"3113586070","duration":14981,"mediaUrl":"http://123.140.50.75/prod/-50430/ENCODING/18/138961747812860183.mp4","playType":"DNP","bytes":5702218}
</Asset>
</Extension>
</Extensions>
</InLine>
</Ad>
</VAST>
Loading…
Cancel
Save